From 0e41f3618906e6a271c685c40d1cf9694961fd33 Mon Sep 17 00:00:00 2001 From: dougeEfresh Date: Tue, 30 Dec 2025 17:18:22 +0000 Subject: [PATCH 1/8] facet --- Cargo.lock | 391 ++++++++++++++++------------ Cargo.toml | 9 +- src/agent/custom_tool.rs | 40 +-- src/agent/hook.rs | 31 +++ src/agent/mcp_config.rs | 2 - src/agent/mod.rs | 157 ++++++------ src/config.rs | 32 +++ src/config/agent.rs | 280 ++++++++++++++++++++ src/config/agent_file.rs | 1 + src/config/hook.rs | 281 ++++++++++++++++++++ src/config/mcp.rs | 201 +++++++++++++++ src/config/merge.rs | 209 +++++++++++++++ src/config/native.rs | 528 ++++++++++++++++++++++++++++++++++++++ src/generator/discover.rs | 10 +- src/generator/merge.rs | 165 ++++++------ src/generator/mod.rs | 10 +- src/kdl/mcp.rs | 9 - src/kdl/merge.rs | 339 ++++++++++++------------ src/kdl/mod.rs | 474 +++++++++++++++++----------------- src/kdl/native.rs | 480 ---------------------------------- src/main.rs | 3 +- 21 files changed, 2378 insertions(+), 1274 deletions(-) create mode 100644 src/config.rs create mode 100644 src/config/agent.rs create mode 100644 src/config/agent_file.rs create mode 100644 src/config/hook.rs create mode 100644 src/config/mcp.rs create mode 100644 src/config/merge.rs create mode 100644 src/config/native.rs delete mode 100644 src/kdl/native.rs diff --git a/Cargo.lock b/Cargo.lock index 8392a31..d552e9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,7 +135,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -174,12 +174,6 @@ dependencies = [ "backtrace", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -253,15 +247,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chumsky" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" -dependencies = [ - "hashbrown 0.14.5", -] - [[package]] name = "clap" version = "4.5.53" @@ -292,10 +277,10 @@ version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -315,7 +300,7 @@ dependencies = [ "eyre", "indenter", "once_cell", - "owo-colors 4.2.3", + "owo-colors", "tracing-error", "url", ] @@ -327,7 +312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" dependencies = [ "once_cell", - "owo-colors 4.2.3", + "owo-colors", "tracing-core", "tracing-error", ] @@ -434,7 +419,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -493,7 +478,7 @@ checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -522,6 +507,116 @@ dependencies = [ "once_cell", ] +[[package]] +name = "facet" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ee49c69f8a398d01d9b160e3e6288c1a5f7d756e8377f0530bbb4019aa1616" +dependencies = [ + "autocfg", + "facet-core", + "facet-macros", + "static_assertions", +] + +[[package]] +name = "facet-core" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ca85b6f8c289d86e5a0daa6b402ed1edf4001ad9b6ead357cc047fff680e0d" +dependencies = [ + "autocfg", + "impls", +] + +[[package]] +name = "facet-kdl" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b52d9f35c93a85109d9d1d9042fbc1b02972c28eaceb36374995e9d166f4019" +dependencies = [ + "facet", + "facet-core", + "facet-reflect", + "facet-singularize", + "facet-solver", + "kdl", + "log", + "miette", +] + +[[package]] +name = "facet-macro-parse" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "294183c810413075f9c3f075c0b3554d04ad06207dad18debc649a48779321f6" +dependencies = [ + "facet-macro-types", + "proc-macro2", + "quote", +] + +[[package]] +name = "facet-macro-types" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8335dd3290eb5780aa40fb5b0da6c1a1c08980af6ada54c2e0d8cbbcd52b8f33" +dependencies = [ + "proc-macro2", + "quote", + "unsynn", +] + +[[package]] +name = "facet-macros" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f7c8e20f24f6c933290da20e76ce8b62a28ea7f16ea173a1aa21cb2ebf61f0" +dependencies = [ + "facet-macros-impl", +] + +[[package]] +name = "facet-macros-impl" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc36ba0859bb5fc539e9fb9ed4dab7a5af3b9dbf080e92adaeb5041c58971fcb" +dependencies = [ + "facet-macro-parse", + "facet-macro-types", + "proc-macro2", + "quote", + "strsim", + "unsynn", +] + +[[package]] +name = "facet-reflect" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ab47f7ccaed7b782b4cdbfa3482f16720c0e7e31c38bf5f7da8b8f8c988690" +dependencies = [ + "facet-core", + "miette", +] + +[[package]] +name = "facet-singularize" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd14cadea48902b862d1f9256f1eac9102680d3fc105e5888008e219f2de6023" + +[[package]] +name = "facet-solver" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a23bcde5d4f562dfed81ca31c15c241536b1b6ef0a6e46cc17d8963b9f9f33" +dependencies = [ + "facet-core", + "facet-reflect", + "strsim", +] + [[package]] name = "fancy-regex" version = "0.16.2" @@ -658,7 +753,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -771,27 +866,12 @@ dependencies = [ "hashbrown 0.14.5", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "http" version = "1.4.0" @@ -891,7 +971,7 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-core", @@ -1013,6 +1093,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "impls" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a46645bbd70538861a90d0f26c31537cdf1e44aae99a794fb75a664b70951bc" + [[package]] name = "indenter" version = "0.3.4" @@ -1029,6 +1115,15 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1045,17 +1140,6 @@ dependencies = [ "serde", ] -[[package]] -name = "is-terminal" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "is_ci" version = "1.2.0" @@ -1114,6 +1198,17 @@ dependencies = [ "uuid-simd", ] +[[package]] +name = "kdl" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81a29e7b50079ff44549f68c0becb1c73d7f6de2a4ea952da77966daf3d4761e" +dependencies = [ + "miette", + "num", + "winnow", +] + [[package]] name = "kiro-generator" version = "0.1.1-rc.6" @@ -1124,9 +1219,11 @@ dependencies = [ "dirs", "emojis-rs", "enum-iterator", + "facet", + "facet-kdl", "futures", + "indoc", "jsonschema", - "knuffel", "miette", "nix", "serde", @@ -1141,33 +1238,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "knuffel" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04bee6ddc6071011314b1ce4f7705fef6c009401dba4fd22cb0009db6a177413" -dependencies = [ - "base64 0.21.7", - "chumsky", - "knuffel-derive", - "miette", - "thiserror 1.0.69", - "unicode-width 0.1.14", -] - -[[package]] -name = "knuffel-derive" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91977f56c49cfb961e3d840e2e7c6e4a56bde7283898cf606861f1421348283d" -dependencies = [ - "heck 0.4.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -1240,34 +1310,32 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miette" -version = "5.10.0" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" dependencies = [ "backtrace", "backtrace-ext", - "is-terminal", + "cfg-if", "miette-derive", - "once_cell", - "owo-colors 3.5.0", + "owo-colors", "supports-color", "supports-hyperlinks", "supports-unicode", "terminal_size", "textwrap", - "thiserror 1.0.69", "unicode-width 0.1.14", ] [[package]] name = "miette-derive" -version = "5.10.0" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -1302,6 +1370,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "mutants" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126" + [[package]] name = "native-tls" version = "0.2.14" @@ -1473,7 +1547,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -1506,12 +1580,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" -[[package]] -name = "owo-colors" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" - [[package]] name = "owo-colors" version = "4.2.3" @@ -1574,30 +1642,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.103" @@ -1659,7 +1703,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -1714,7 +1758,7 @@ version = "0.12.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "encoding_rs", "futures-channel", @@ -1770,6 +1814,12 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "1.1.2" @@ -1893,7 +1943,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -1959,12 +2009,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - [[package]] name = "socket2" version = "0.6.1" @@ -1981,6 +2025,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -2008,42 +2058,24 @@ dependencies = [ [[package]] name = "supports-color" -version = "2.1.0" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" dependencies = [ - "is-terminal", "is_ci", ] [[package]] name = "supports-hyperlinks" -version = "2.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" -dependencies = [ - "is-terminal", -] +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" [[package]] name = "supports-unicode" -version = "2.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f850c19edd184a205e883199a261ed44471c81e39bd95b1357f5febbef00e77a" -dependencies = [ - "is-terminal", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" @@ -2073,7 +2105,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -2112,12 +2144,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.1.17" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "libc", - "winapi", + "rustix", + "windows-sys 0.60.2", ] [[package]] @@ -2138,18 +2170,17 @@ checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] name = "textwrap" -version = "0.15.2" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ - "smawk", "unicode-linebreak", - "unicode-width 0.1.14", + "unicode-width 0.2.2", ] [[package]] @@ -2178,7 +2209,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -2189,7 +2220,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -2234,7 +2265,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -2334,7 +2365,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -2434,6 +2465,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unsynn" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501a7adf1a4bd9951501e5c66621e972ef8874d787628b7f90e64f936ef7ec0a" +dependencies = [ + "mutants", + "proc-macro2", + "rustc-hash", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -2577,7 +2619,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn", "wasm-bindgen-shared", ] @@ -2813,6 +2855,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.6.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -2855,7 +2906,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", "synstructure", ] @@ -2876,7 +2927,7 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -2896,7 +2947,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", "synstructure", ] @@ -2936,5 +2987,5 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index bb0a947..a6459ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,10 +33,12 @@ dirs = "6" # emojis = "0.8.0" emojis-rs = "0.1.3" enum-iterator = "2.3.0" +facet = "0.34.0" +facet-kdl = "0.34.0" futures = "0.3" +indoc = "2.0.7" jsonschema = { version = "0.37", default-features = false, features = ["resolve-async", "resolve-file"] } -knuffel = "3.2.0" -miette = { version = "5", features = ["fancy"] } +miette = { version = "7", features = ["fancy"] } serde = { version = "1", features = ["derive"] } serde_json = { version = "1" } serde_yaml2 = "0.1.3" @@ -56,3 +58,6 @@ strip = false # Keep symbols for better backtraces debug = 1 # Line numbers only (smaller than full debug=2) lto = "thin" # Faster builds than "fat", still good optimization codegen-units = 16 # Default, balances compile time vs optimization + +[lints.rust] +unused_imports = "allow" diff --git a/src/agent/custom_tool.rs b/src/agent/custom_tool.rs index 6796dd7..3fc81f6 100644 --- a/src/agent/custom_tool.rs +++ b/src/agent/custom_tool.rs @@ -3,40 +3,15 @@ use { std::collections::HashMap, }; -#[derive(Clone, Default, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum TransportType { - /// Standard input/output transport (default) - #[default] - Stdio, - /// HTTP transport for web-based communication - Http, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct OAuthConfig { - /// Custom redirect URI for OAuth flow (e.g., "127.0.0.1:7778") - /// If not specified, a random available port will be assigned by the OS - #[serde(skip_serializing_if = "Option::is_none")] - pub redirect_uri: Option, -} - #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomToolConfig { - /// The transport type to use for communication with the MCP server - #[serde(default)] - pub r#type: TransportType, /// The URL for HTTP-based MCP server communication #[serde(default, skip_serializing_if = "String::is_empty")] pub url: String, /// HTTP headers to include when communicating with HTTP-based MCP servers #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub headers: HashMap, - /// OAuth configuration for this server - #[serde(skip_serializing_if = "Option::is_none")] - pub oauth: Option, /// The command string used to initialize the mcp server #[serde(default)] pub command: String, @@ -63,18 +38,15 @@ mod tests { use super::*; #[test] - fn transport_type_default() { + fn tool_defaultttl() { assert_eq!(tool_default_timeout(), 120 * 1000); - assert_eq!(TransportType::default(), TransportType::Stdio); } #[test] fn custom_tool_config_serde() { let config = CustomToolConfig { - r#type: TransportType::Http, url: "http://test".into(), headers: HashMap::new(), - oauth: None, command: "cmd".into(), args: vec!["arg1".into()], env: HashMap::new(), @@ -85,14 +57,4 @@ mod tests { let deserialized: CustomToolConfig = serde_json::from_str(&json).unwrap(); assert_eq!(config, deserialized); } - - #[test] - fn oauth_config_serde() { - let oauth = OAuthConfig { - redirect_uri: Some("localhost:8080".into()), - }; - let json = serde_json::to_string(&oauth).unwrap(); - let deserialized: OAuthConfig = serde_json::from_str(&json).unwrap(); - assert_eq!(oauth, deserialized); - } } diff --git a/src/agent/hook.rs b/src/agent/hook.rs index 886495b..4c12ab3 100644 --- a/src/agent/hook.rs +++ b/src/agent/hook.rs @@ -60,6 +60,37 @@ pub struct Hook { } impl Hook { + pub fn merge(mut self, o: Self) -> Self { + if self.cache_ttl_seconds == 0 { + self.cache_ttl_seconds = if o.cache_ttl_seconds == 0 { + DEFAULT_CACHE_TTL_SECONDS + } else { + o.cache_ttl_seconds + }; + } + if self.command.is_empty() { + self.command = o.command; + } + if self.max_output_size == 0 { + self.max_output_size = if o.max_output_size == 0 { + DEFAULT_MAX_OUTPUT_SIZE + } else { + o.max_output_size + }; + } + if self.timeout_ms == 0 { + self.timeout_ms = if o.timeout_ms == 0 { + DEFAULT_TIMEOUT_MS + } else { + o.timeout_ms + }; + } + if self.matcher.is_none() && o.matcher.is_some() { + self.matcher = o.matcher; + } + self + } + fn default_timeout_ms() -> u64 { DEFAULT_TIMEOUT_MS } diff --git a/src/agent/mcp_config.rs b/src/agent/mcp_config.rs index 743e948..07e712b 100644 --- a/src/agent/mcp_config.rs +++ b/src/agent/mcp_config.rs @@ -24,10 +24,8 @@ mod tests { fn mcp_server_config_serde() { let mut config = McpServerConfig::default(); config.mcp_servers.insert("test".into(), CustomToolConfig { - r#type: Default::default(), url: String::new(), headers: HashMap::new(), - oauth: None, command: "cmd".into(), args: vec![], env: HashMap::new(), diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 62be2ae..bcbb666 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -7,7 +7,7 @@ pub const DEFAULT_AGENT_RESOURCES: &[&str] = &["file://README.md", "file://AGENT pub const DEFAULT_APPROVE: [&str; 0] = []; use { super::agent::hook::{Hook, HookTrigger}, - crate::{Result, kdl::KdlAgent}, + crate::{Result, config::KdlAgent}, color_eyre::eyre::eyre, serde::{Deserialize, Serialize}, std::{ @@ -16,7 +16,7 @@ use { }, }; pub use { - custom_tool::{CustomToolConfig, OAuthConfig, TransportType, tool_default_timeout}, + custom_tool::{CustomToolConfig, tool_default_timeout}, mcp_config::McpServerConfig, tools::*, wrapper_types::OriginalToolName, @@ -97,83 +97,88 @@ impl TryFrom<&KdlAgent> for Agent { type Error = color_eyre::Report; fn try_from(value: &KdlAgent) -> std::result::Result { - let native_tools = &value.native_tool; - let mut tools_settings = HashMap::new(); + Ok(Self::default()) + } - let tool: AwsTool = native_tools.into(); - let tool_name = ToolTarget::Aws.to_string(); - if tool != AwsTool::default() { - tools_settings.insert( - tool_name.to_string(), - serde_json::to_value(&tool) - .map_err(|e| eyre!("Failed to serialize {tool_name} tool configuration {e}"))?, - ); - } - let tool: ReadTool = native_tools.into(); - let tool_name = ToolTarget::Read.to_string(); - if tool != ReadTool::default() { - tools_settings.insert( - tool_name.to_string(), - serde_json::to_value(&tool) - .map_err(|e| eyre!("Failed to serialize {tool_name} tool configuration {e}"))?, - ); - } - let tool: WriteTool = native_tools.into(); - let tool_name = ToolTarget::Write.to_string(); - if tool != WriteTool::default() { - tools_settings.insert( - tool_name.to_string(), - serde_json::to_value(&tool) - .map_err(|e| eyre!("Failed to serialize {tool_name} tool configuration {e}"))?, - ); - } - let tool: ExecuteShellTool = native_tools.into(); - let tool_name = ToolTarget::Shell.to_string(); - if tool != ExecuteShellTool::default() { - tools_settings.insert( - tool_name.to_string(), - serde_json::to_value(&tool) - .map_err(|e| eyre!("Failed to serialize {tool_name} tool configuration {e}"))?, - ); - } - let default_agent = Self::default(); - let tools = value.tools().clone(); - let allowed_tools = value.allowed_tools().clone(); - let resources: HashSet = value.resources().map(|s| s.to_string()).collect(); + // fn try_from(value: &KdlAgent) -> std::result::Result { + // let native_tools = &value.native_tool; + // let mut tools_settings = HashMap::new(); - // Extra tool settings override native tools - let extra_tool_settings = value.extra_tool_settings()?; - tools_settings.extend(extra_tool_settings); + // let tool: AwsTool = native_tools.into(); + // let tool_name = ToolTarget::Aws.to_string(); + // if tool != AwsTool::default() { + // tools_settings.insert( + // tool_name.to_string(), + // serde_json::to_value(&tool) + // .map_err(|e| eyre!("Failed to serialize {tool_name} tool + // configuration {e}"))?, ); + // } + // let tool: ReadTool = native_tools.into(); + // let tool_name = ToolTarget::Read.to_string(); + // if tool != ReadTool::default() { + // tools_settings.insert( + // tool_name.to_string(), + // serde_json::to_value(&tool) + // .map_err(|e| eyre!("Failed to serialize {tool_name} tool + // configuration {e}"))?, ); + // } + // let tool: WriteTool = native_tools.into(); + // let tool_name = ToolTarget::Write.to_string(); + // if tool != WriteTool::default() { + // tools_settings.insert( + // tool_name.to_string(), + // serde_json::to_value(&tool) + // .map_err(|e| eyre!("Failed to serialize {tool_name} tool + // configuration {e}"))?, ); + // } + // let tool: ExecuteShellTool = native_tools.into(); + // let tool_name = ToolTarget::Shell.to_string(); + // if tool != ExecuteShellTool::default() { + // tools_settings.insert( + // tool_name.to_string(), + // serde_json::to_value(&tool) + // .map_err(|e| eyre!("Failed to serialize {tool_name} tool + // configuration {e}"))?, ); + // } + // let default_agent = Self::default(); + // let tools = value.tools().clone(); + // let allowed_tools = value.allowed_tools().clone(); + // let resources: HashSet = value.resources().map(|s| + // s.to_string()).collect(); - Ok(Self { - name: value.name.clone(), - description: value.description.clone(), - prompt: value.prompt.clone(), - mcp_servers: McpServerConfig { - mcp_servers: value.mcp_servers(), - }, - tools: if tools.is_empty() { - default_agent.tools - } else { - tools - }, - tool_aliases: value.tool_aliases(), - allowed_tools: if allowed_tools.is_empty() { - default_agent.allowed_tools - } else { - allowed_tools - }, - resources: if resources.is_empty() { - default_agent.resources - } else { - resources - }, - hooks: value.hooks(), - tools_settings, - model: value.model.clone(), - include_mcp_json: value.include_mcp_json(), - }) - } + // // Extra tool settings override native tools + // let extra_tool_settings = value.extra_tool_settings()?; + // tools_settings.extend(extra_tool_settings); + + // Ok(Self { + // name: value.name.clone(), + // description: value.description.clone(), + // prompt: value.prompt.clone(), + // mcp_servers: McpServerConfig { + // mcp_servers: value.mcp_servers(), + // }, + // tools: if tools.is_empty() { + // default_agent.tools + // } else { + // tools + // }, + // tool_aliases: value.tool_aliases(), + // allowed_tools: if allowed_tools.is_empty() { + // default_agent.allowed_tools + // } else { + // allowed_tools + // }, + // resources: if resources.is_empty() { + // default_agent.resources + // } else { + // resources + // }, + // hooks: value.hooks(), + // tools_settings, + // model: value.model.clone(), + // include_mcp_json: value.include_mcp_json(), + // }) + // } } impl Default for Agent { diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..cedf093 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,32 @@ +mod agent; +mod agent_file; +mod hook; +mod mcp; +mod merge; +mod native; + +use std::{collections::HashSet, fmt::Debug}; + +pub use {agent::KdlAgent, hook::HookPart, mcp::CustomToolConfigKdl, native::NativeTools}; + +#[derive(facet::Facet, Default)] +pub struct GeneratorConfig { + #[facet(facet_kdl::children, default)] + pub agent: Vec, +} + +impl Debug for GeneratorConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "agents={}", self.agent.len()) + } +} + +impl GeneratorConfig { + pub fn names(&self) -> HashSet { + self.agent.iter().map(|a| a.name.clone()).collect() + } + + pub fn get(&self, name: impl AsRef) -> Option<&KdlAgent> { + self.agent.iter().find(|a| a.name.eq(name.as_ref())) + } +} diff --git a/src/config/agent.rs b/src/config/agent.rs new file mode 100644 index 0000000..b475228 --- /dev/null +++ b/src/config/agent.rs @@ -0,0 +1,280 @@ +use { + super::{ + hook::HookDoc, + mcp::CustomToolConfigKdl, + native::{AwsTool, ExecuteShellTool, NativeTools, NativeToolsDoc, ReadTool, WriteTool}, + }, + crate::agent::{ + CustomToolConfig, OriginalToolName, + hook::{Hook, HookTrigger}, + }, + color_eyre::eyre::WrapErr, + facet::Facet, + facet_kdl as kdl, + std::{ + collections::{HashMap, HashSet}, + fmt::{Debug, Display}, + hash::Hash, + }, +}; + +#[derive(Facet, Clone, Default, Debug)] +pub(super) struct Inherits { + #[facet(kdl::arguments)] + pub parents: Vec, +} + +#[derive(Facet, Clone, Default, Debug)] +pub(super) struct Tools { + #[facet(kdl::arguments)] + pub tools: Vec, +} + +#[derive(Facet, Clone, Default, Debug)] +pub(super) struct AllowedTools { + #[facet(kdl::arguments)] + pub allowed: Vec, +} + +#[derive(Facet, Clone, Default, Debug)] +pub(super) struct Resource { + #[facet(kdl::argument)] + pub location: String, +} + +impl PartialEq for Resource { + fn eq(&self, other: &Self) -> bool { + self.location.eq(&other.location) + } +} + +impl Hash for Resource { + fn hash(&self, state: &mut H) { + self.location.hash(state); + } +} +impl Eq for Resource {} + +#[derive(Facet, Clone, Default, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub(super) struct ToolAliasKdl { + #[facet(kdl::argument)] + from: String, + #[facet(kdl::argument)] + to: String, +} + +#[derive(Facet, Clone, Debug)] +pub struct ToolSetting { + #[facet(kdl::argument)] + name: String, + #[facet(kdl::child)] + json: Json, +} + +#[derive(Facet, Clone, Debug)] +struct Json { + #[facet(kdl::argument)] + value: String, +} + +impl ToolSetting { + fn to_value(&self) -> crate::Result<(String, serde_json::Value)> { + let v: serde_json::Value = serde_json::from_str(&self.json.value) + .wrap_err_with(|| format!("Failed to parse JSON for tool-setting '{}'", self.name))?; + + if !v.is_object() { + return Err(color_eyre::eyre::eyre!( + "tool-setting '{}' must be a JSON object, got: {}", + self.name, + v + )); + } + + Ok((self.name.clone(), v)) + } +} + +#[derive(Facet, Clone, Default)] +pub struct KdlAgentDoc { + #[facet(kdl::argument)] + pub name: String, + #[facet(kdl::property)] + pub template: Option, + #[facet(kdl::child, default)] + pub description: Option, + #[facet(kdl::child, default)] + pub(super) inherits: Inherits, + #[facet(kdl::child, default)] + pub prompt: Option, + #[facet(kdl::children, default)] + pub(super) resources: Vec, + #[facet(kdl::child, default)] + pub include_mcp_json: Option, + #[facet(kdl::child, default)] + pub(super) tools: Option, + #[facet(kdl::child, default)] + pub(super) allowed_tools: Option, + #[facet(kdl::child, default)] + pub model: Option, + #[facet(kdl::child, default)] + pub(super) hook: Option, + #[facet(kdl::children, default)] + pub(super) mcp: Vec, + #[facet(kdl::children, default)] + pub(super) alias: Vec, + #[facet(kdl::child, default)] + pub native_tool: Option, + #[facet(kdl::children, default)] + pub(super) tool_setting: Vec, +} + +#[derive(Facet, Clone, Default, Debug)] +pub struct Description { + #[facet(kdl::argument)] + value: String, +} + +#[derive(Facet, Clone, Default, Debug)] +struct Prompt { + #[facet(kdl::argument)] + value: String, +} + +#[derive(Facet, Clone, Debug)] +struct IncludeMcpJson { + #[facet(kdl::argument)] + value: bool, +} + +#[derive(Facet, Clone, Debug)] +struct Model { + #[facet(kdl::argument)] + value: String, +} + +impl Debug for KdlAgent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +impl Display for KdlAgent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +impl KdlAgent { + pub fn prompt(&self) -> String { + self.prompt.clone().unwrap_or_default().value + } + + pub fn description(&self) -> String { + self.description.clone().unwrap_or_default().value + } + + pub fn new(name: impl AsRef) -> Self { + Self { + name: name.as_ref().to_string(), + ..Default::default() + } + } + + pub fn is_template(&self) -> bool { + self.template.is_some_and(|f| f) + } + + pub fn include_mcp_json(&self) -> bool { + self.include_mcp_json + .as_ref() + .map(|i| i.value) + .unwrap_or(false) + } + + pub fn get_tool_aws(&self) -> AwsTool { + self.native_tool + .as_ref() + .and_then(|n| n.aws.clone()) + .unwrap_or_default() + } + + pub fn get_tool_read(&self) -> ReadTool { + self.native_tool + .as_ref() + .and_then(|n| n.read.clone()) + .unwrap_or_default() + } + + pub fn get_tool_write(&self) -> WriteTool { + self.native_tool + .as_ref() + .and_then(|n| n.write.clone()) + .unwrap_or_default() + } + + pub fn get_tool_shell(&self) -> ExecuteShellTool { + self.native_tool + .as_ref() + .and_then(|n| n.shell.clone()) + .unwrap_or_default() + } + + pub fn tool_aliases(&self) -> HashMap { + self.alias + .iter() + .map(|m| (OriginalToolName(m.from.clone()), m.to.clone())) + .collect() + } + + pub fn hooks(&self) -> HashMap> { + match &self.hook { + None => HashMap::new(), + Some(h) => h.triggers(), + } + } + + pub fn allowed_tools(&self) -> HashSet { + self.allowed_tools + .as_ref() + .map(|a| HashSet::from_iter(a.allowed.clone())) + .unwrap_or_default() + } + + pub fn tools(&self) -> HashSet { + self.tools + .as_ref() + .map(|t| HashSet::from_iter(t.tools.clone())) + .unwrap_or_default() + } + + pub fn inherits(&self) -> HashSet { + HashSet::from_iter(self.inherits.parents.clone()) + } + + pub fn resources(&self) -> impl Iterator { + self.resources.iter().map(|r| r.location.as_str()) + } + + pub fn mcp_servers(&self) -> HashMap { + self.mcp + .iter() + .map(|m| (m.name.clone(), m.into())) + .collect() + } + + pub fn extra_tool_settings(&self) -> crate::Result> { + let mut result = HashMap::new(); + for setting in &self.tool_setting { + let (name, value) = setting.to_value()?; + if result.contains_key(&name) { + return Err(color_eyre::eyre::eyre!( + "[{self}] - Duplicate tool-setting '{}' found. Each tool-setting name must be \ + unique.", + name + )); + } + result.insert(name, value); + } + Ok(result) + } +} diff --git a/src/config/agent_file.rs b/src/config/agent_file.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/config/agent_file.rs @@ -0,0 +1 @@ + diff --git a/src/config/hook.rs b/src/config/hook.rs new file mode 100644 index 0000000..fc4eedb --- /dev/null +++ b/src/config/hook.rs @@ -0,0 +1,281 @@ +use { + crate::agent::hook::{Hook, HookTrigger}, + facet::Facet, + facet_kdl as kdl, + std::collections::HashMap, +}; + +#[derive(Facet, Clone, Debug, PartialEq, Eq)] +struct Command { + #[facet(kdl::argument)] + value: String, +} + +#[derive(Facet, Clone, Debug, PartialEq, Eq)] +struct TimeoutMs { + #[facet(kdl::argument)] + value: usize, +} + +#[derive(Facet, Clone, Debug, PartialEq, Eq)] +struct MaxOutputSize { + #[facet(kdl::argument)] + value: usize, +} + +#[derive(Facet, Clone, Debug, PartialEq, Eq)] +struct CacheTtlSeconds { + #[facet(kdl::argument)] + value: usize, +} + +#[derive(Facet, Clone, Debug, PartialEq, Eq)] +struct Matcher { + #[facet(kdl::argument)] + value: String, +} + +macro_rules! define_hook_doc { + ($name:ident) => { + #[derive(Facet, Default, Clone, Debug, PartialEq, Eq)] + #[facet(default, rename_all = "kebab-case")] + pub struct $name { + #[facet(kdl::argument)] + pub name: String, + #[facet(kdl::child, default)] + command: GenericValue, + #[facet(default, kdl::child)] + timeout_ms: IntDoc, + #[facet(kdl::child, default)] + max_output_size: IntDoc, + #[facet(kdl::child, default)] + cache_ttl_seconds: IntDoc, + #[facet(kdl::child, default)] + matcher: Option, + } + impl From<$name> for Hook { + fn from(value: $name) -> Hook { + Hook { + command: value.command.value, + timeout_ms: value.timeout_ms.value as u64, + max_output_size: value.max_output_size.value, + cache_ttl_seconds: value.cache_ttl_seconds.value as u64, + matcher: value.matcher.map(|m| m.value), + } + } + } + }; +} + +macro_rules! define_hook { + ($name:ident) => { + #[derive(Default, Clone, Debug, PartialEq, Eq)] + pub struct $name { + #[facet(kdl::argument)] + pub name: String, + command: String, + timeout_ms: u64, + max_output_size: u64, + cache_ttl_seconds: u64, + matcher: Option, + } + }; +} + +#[derive(Facet, Clone, Default, Debug, PartialEq, Eq)] +#[facet(default)] +struct GenericValue { + #[facet(kdl::argument)] + value: String, +} + +#[derive(Facet, Default, Clone, Debug, PartialEq, Eq)] +#[facet(default)] +struct IntDoc { + #[facet(kdl::argument)] + value: usize, +} + +define_hook_doc!(HookAgentSpawnDoc); +define_hook_doc!(HookUserPromptSubmitDoc); +define_hook_doc!(HookPreToolUseDoc); +define_hook_doc!(HookPostToolUseDoc); +define_hook_doc!(HookStopDoc); + +#[derive(Facet, Clone, Default, Debug, PartialEq, Eq)] +#[facet(default, rename_all = "kebab-case")] +pub struct HookDoc { + #[facet(kdl::children, default)] + pub agent_spawn: Vec, + #[facet(kdl::children, default)] + pub user_prompt_submit: Vec, + #[facet(kdl::children, default)] + pub pre_tool_use: Vec, + #[facet(kdl::children, default)] + pub post_tool_use: Vec, + #[facet(kdl::children, default)] + pub stop: Vec, +} + +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct HookPart { + pub agent_spawn: HashMap, + pub user_prompt_submit: HashMap, + pub pre_tool_use: HashMap, + pub post_tool_use: HashMap, + pub stop: HashMap, +} + +impl From for HookPart { + fn from(value: HookDoc) -> Self { + Self { + agent_spawn: value + .agent_spawn + .into_iter() + .map(|h| (h.name.clone(), Hook::from(h))) + .collect(), + user_prompt_submit: value + .user_prompt_submit + .into_iter() + .map(|h| (h.name.clone(), Hook::from(h))) + .collect(), + pre_tool_use: value + .pre_tool_use + .into_iter() + .map(|h| (h.name.clone(), Hook::from(h))) + .collect(), + post_tool_use: value + .post_tool_use + .into_iter() + .map(|h| (h.name.clone(), Hook::from(h))) + .collect(), + stop: value + .stop + .into_iter() + .map(|h| (h.name.clone(), Hook::from(h))) + .collect(), + } + } +} +impl HookPart { + pub fn merge(mut self, other: Self) -> Self { + match (self.agent_spawn.is_empty(), other.agent_spawn.is_empty()) { + (false, false) => { + let mut hooks = HashMap::with_capacity(self.agent_spawn.len()); + for (k, h) in self.agent_spawn { + if let Some(o) = other.agent_spawn.get(&k) { + hooks.insert(k.to_string(), h.merge(o.clone())); + } else { + hooks.insert(k, h); + } + } + self.agent_spawn = hooks; + for o in other.agent_spawn.keys() { + if !self.agent_spawn.contains_key(o) { + self.agent_spawn + .insert(o.to_string(), other.agent_spawn.get(o).unwrap().clone()); + } + } + } + (true, false) => self.agent_spawn = other.agent_spawn, + _ => {} + }; + self + } +} + +// #[cfg(test)] +// mod tests { +// use {super::*, crate::Result, std::time::Duration}; + +// macro_rules! rando_hook { +// ($name:ident) => { +// impl $name { +// fn rando() -> $name { +// let value = std::time::SystemTime::now() +// .duration_since(std::time::UNIX_EPOCH) +// .unwrap() +// .as_secs(); +// Self { +// name: format!("$name-{value}"), +// command: Some(Command { +// value: format!("{value}"), +// }), +// timeout_ms: Some(TimeoutMs { value }), +// max_output_size: None, +// cache_ttl_seconds: Some(CacheTtlSeconds { value }), +// matcher: Some(Matcher { +// value: format!("{value}"), +// }), +// } +// } +// } +// }; +// } +// rando_hook!(HookAgentSpawn); +// rando_hook!(HookUserPromptSubmit); +// rando_hook!(HookPreToolUse); +// rando_hook!(HookPostToolUse); +// rando_hook!(HookStop); + +// impl HookPart { +// pub fn randomize() -> Self { +// Self { +// agent_spawn: vec![HookAgentSpawn::rando()], +// user_prompt_submit: vec![HookUserPromptSubmit::rando()], +// pre_tool_use: vec![HookPreToolUse::rando()], +// post_tool_use: vec![HookPostToolUse::rando()], +// stop: vec![HookStop::rando()], +// } +// } +// } + +// #[test_log::test] +// pub fn test_hooks_empty() -> Result<()> { +// let child = HookPart::default(); +// let parent = HookPart::default(); +// let merged = child.merge(parent); + +// assert!(merged.agent_spawn.is_empty()); +// assert!(merged.user_prompt_submit.is_empty()); +// assert!(merged.pre_tool_use.is_empty()); +// assert!(merged.post_tool_use.is_empty()); +// assert!(merged.stop.is_empty()); +// Ok(()) +// } + +// #[test_log::test] +// pub fn test_hooks_empty_child() -> Result<()> { +// let child = HookPart::default(); +// let parent = HookPart::randomize(); +// let before = parent.clone(); +// let merged = child.merge(parent); + +// assert_eq!(merged, before); +// Ok(()) +// } + +// #[test_log::test] +// pub fn test_hooks_no_merge() -> Result<()> { +// let child = HookPart::randomize(); +// let parent = HookPart::randomize(); +// let before = child.clone(); +// let merged = child.merge(parent); +// assert_eq!(merged, before); +// Ok(()) +// } + +// #[test_log::test] +// pub fn test_hooks_merge_parent() -> Result<()> { +// let child = HookPart::randomize(); +// std::thread::sleep(Duration::from_millis(1300)); +// let parent = HookPart::randomize(); +// let merged = child.merge(parent); +// assert_eq!(merged.agent_spawn.len(), 2); +// assert_eq!(merged.user_prompt_submit.len(), 2); +// assert_eq!(merged.pre_tool_use.len(), 2); +// assert_eq!(merged.post_tool_use.len(), 2); +// assert_eq!(merged.stop.len(), 2); +// Ok(()) +// } +// } diff --git a/src/config/mcp.rs b/src/config/mcp.rs new file mode 100644 index 0000000..fa0f182 --- /dev/null +++ b/src/config/mcp.rs @@ -0,0 +1,201 @@ +use {crate::agent::CustomToolConfig, facet::Facet, facet_kdl as kdl}; + +#[derive(Facet, Clone, Debug)] +struct EnvVar { + #[facet(kdl::argument)] + key: String, + #[facet(kdl::argument)] + value: String, +} + +#[derive(Facet, Clone, Debug)] +struct Header { + #[facet(kdl::argument)] + key: String, + #[facet(kdl::argument)] + value: String, +} + +#[derive(Facet, Default, Clone, Debug)] +struct ToolArgs { + #[facet(kdl::arguments, default)] + args: Vec, +} + +#[derive(Facet, Clone, Debug, Eq, PartialEq)] +pub struct RedirectUri { + #[facet(kdl::argument)] + pub value: String, +} + +#[derive(Facet, Clone, Debug)] +pub struct CustomToolConfigKdl { + #[facet(kdl::argument)] + pub name: String, + + #[facet(kdl::child, default)] + pub url: Option, + + #[facet(kdl::child, default)] + pub command: Option, + + #[facet(kdl::child, default)] + args: Option, + + #[facet(kdl::children, default)] + env: Vec, + + #[facet(kdl::children, default)] + header: Vec
, + + #[facet(kdl::child, default)] + pub timeout: Option, + + #[facet(kdl::child, default)] + pub disabled: Option, +} + +#[derive(Facet, Clone, Debug)] +pub struct Url { + #[facet(kdl::argument)] + pub value: String, +} + +#[derive(Facet, Clone, Debug)] +pub struct Command { + #[facet(kdl::argument)] + pub value: String, +} + +#[derive(Facet, Clone, Debug)] +pub struct Timeout { + #[facet(kdl::argument)] + pub value: u64, +} + +#[derive(Facet, Clone, Debug)] +pub struct Disabled { + #[facet(kdl::argument)] + pub value: bool, +} + +impl From for CustomToolConfig { + fn from(value: CustomToolConfigKdl) -> Self { + let command = value.command.map(|c| c.value).unwrap_or_default(); + let url = value.url.map(|u| u.value).unwrap_or_default(); + + Self { + url, + command, + args: value.args.map(|a| a.args).unwrap_or_default(), + timeout: value + .timeout + .map(|t| t.value) + .filter(|&t| t != 0) + .unwrap_or_else(crate::agent::tool_default_timeout), + disabled: value.disabled.map(|d| d.value).unwrap_or(false), + headers: value.header.into_iter().map(|h| (h.key, h.value)).collect(), + env: value.env.into_iter().map(|e| (e.key, e.value)).collect(), + } + } +} + +impl From<&CustomToolConfigKdl> for CustomToolConfig { + fn from(value: &CustomToolConfigKdl) -> Self { + value.clone().into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Facet, Debug)] + struct McpDoc { + #[facet(kdl::child)] + mcp: CustomToolConfigKdl, + } + + #[test] + fn parse_basic_mcp() { + let kdl = r#"mcp "rustdocs" { + command "rust-docs-mcp" + timeout 1000 + }"#; + + let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); + assert_eq!(doc.mcp.name, "rustdocs"); + assert_eq!(doc.mcp.command.unwrap().value, "rust-docs-mcp"); + assert_eq!(doc.mcp.timeout.unwrap().value, 1000); + } + + #[test] + fn parse_mcp_with_url() { + let kdl = r#"mcp "remote" { + url "http://localhost:8080" + }"#; + + let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); + assert_eq!(doc.mcp.name, "remote"); + assert_eq!(doc.mcp.url.unwrap().value, "http://localhost:8080"); + } + + #[test] + fn parse_mcp_with_env_and_headers() { + let kdl = r#"mcp "api" { + command "api-server" + env "API_KEY" "secret123" + env "DEBUG" "true" + header "Authorization" "Bearer token" + header "Content-Type" "application/json" + }"#; + + let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); + assert_eq!(doc.mcp.env.len(), 2); + assert_eq!(doc.mcp.header.len(), 2); + } + + #[test] + fn parse_mcp_with_args() { + let kdl = r#"mcp "tool" { + command "my-tool" + args "--verbose" "--output" "json" + }"#; + + let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); + assert_eq!(doc.mcp.args.unwrap().args, vec![ + "--verbose", + "--output", + "json" + ]); + } + + #[test] + fn convert_to_custom_tool_config() { + let kdl = r#"mcp "test" { + command "test-cmd" + timeout 5000 + disabled #true + }"#; + + let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); + let config: CustomToolConfig = doc.mcp.into(); + + assert_eq!(config.command, "test-cmd"); + assert_eq!(config.timeout, 5000); + assert!(config.disabled); + } + + #[test] + fn default_timeout_when_zero() { + let kdl = r#"mcp "test" { + command "test-cmd" + timeout 0 + }"#; + + let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); + let config: CustomToolConfig = doc.mcp.into(); + + assert_eq!(config.timeout, crate::agent::tool_default_timeout()); + } +} diff --git a/src/config/merge.rs b/src/config/merge.rs new file mode 100644 index 0000000..d576a2a --- /dev/null +++ b/src/config/merge.rs @@ -0,0 +1,209 @@ +use super::*; + +impl KdlAgent { + pub fn merge(mut self, other: KdlAgent) -> Self { + Self::default() + // // Child wins for explicit values + // self.include_mcp_json = + // self.include_mcp_json.or(other.include_mcp_json); + // self.template = self.template.or(other.template); + // self.description = self.description.or(other.description); + // self.prompt = self.prompt.or(other.prompt); + // self.model = self.model.or(other.model); + + // // Collections are extended (merged) + // self.resources.extend(other.resources); + // self.tools.tools.extend(other.tools.tools); + // self.allowed_tools + // .allowed + // .extend(other.allowed_tools.allowed); + // self.tool_aliases.extend(other.tool_aliases); + // self.mcp.extend(other.mcp); + // self.inherits.parents.extend(other.inherits.parents); + // self.tool_settings.extend(other.tool_settings); + + // // Hooks are deep merged + // self.hook = match (self.hook, other.hook) { + // (None, Some(h)) => Some(h), + // (Some(a), Some(b)) => Some(a.merge(b)), + // (Some(a), None) => Some(a), + // (None, None) => None, + // }; + + // // Native tools are deep merged + // self.native_tool = self.native_tool.merge(other.native_tool); + + // self + } +} + +// #[cfg(test)] +// mod tests { +// use {super::*, crate::agent::hook::HookTrigger, color_eyre::eyre::eyre, +// knuffel::parse}; + +// #[test_log::test] +// fn test_agent_merge() -> crate::Result<()> { +// let kdl_agents = r#" +// agent "child" template=$false { +// description "I am a child" +// resource "file://child.md" +// resource "file://README.md" +// inherits "parent" +// include-mcp-json true +// tools "@awsdocs" "shell" +// native-tool { +// write { +// override "Cargo.lock" +// } +// shell { +// override "git push .*" +// } +// } +// hook { +// agent-spawn "spawn" { +// command "echo i have spawned" +// max-output-size 9000 +// cache-ttl-seconds 2 +// } +// } +// alias "execute_bash" "shell" +// } +// agent "parent" template=#true { +// description "I am parent" +// resource "file://parent.md" +// resource "file://README.md" +// tools "web_search" "shell" +// prompt "i tell you what to do" +// model "claude" +// allowed-tools "write" +// alias "execute_bash" "shell" +// alias "fs_read" "read" +// native-tool { +// read { +// allow "./src/*" "./scripts/**" +// deny "Cargo.lock" +// } +// write { +// allow "./src/*" "./scripts/**" +// deny "Cargo.lock" +// } + +// shell { +// allow "git status .*" "git pull .*" +// deny "git push .*" +// } +// } +// hook { +// agent-spawn "spawn" { +// timeout-ms 1111 +// } +// user-prompt-submit "submit" { +// command "echo user submitted" +// timeout-ms 1000 +// } +// pre-tool-use "pre" { +// command "echo before tool" +// matcher "git.*" +// } +// post-tool-use "post" { +// command "echo after tool" +// } +// stop "stop" { +// command "echo stopped" +// } +// } +// } +// "#; + +// let config: GeneratorConfig = match parse("example.kdl", kdl_agents) +// { Ok(c) => c, +// Err(e) => { +// eprintln!("{:?}", miette::Report::new(e)); +// return Err(eyre!("failed to parse {kdl_agents}")); +// } +// }; +// assert_eq!(config.agents.len(), 2); +// let child = config +// .agents +// .iter() +// .find(|a| a.name == "child") +// .unwrap() +// .clone(); +// let parent = config +// .agents +// .iter() +// .find(|a| a.name == "parent") +// .unwrap() +// .clone(); +// let merged = child.merge(parent); +// assert!(merged.description.is_some()); +// let d = merged.description.clone().unwrap(); +// assert_eq!(d, "I am a child"); + +// assert_eq!(merged.resources.len(), 3); +// assert!(!merged.is_template()); +// assert!(merged.include_mcp_json()); + +// assert_eq!(merged.inherits.parents.len(), 1); +// assert!(merged.inherits.parents.contains("parent")); + +// assert_eq!(merged.prompt, Some("i tell you what to do".to_string())); +// let tools = merged.tools(); +// assert_eq!(tools.len(), 3); +// assert!(tools.contains("@awsdocs")); +// assert!(tools.contains("shell")); +// assert!(tools.contains("web_search")); + +// assert_eq!(merged.model, Some("claude".to_string())); + +// let allowed_tools = merged.allowed_tools(); +// assert_eq!(allowed_tools.len(), 1); +// assert!(allowed_tools.contains("write")); + +// let hooks = merged.hooks(); +// assert!(!hooks.is_empty()); +// let h = hooks.get(&HookTrigger::AgentSpawn); +// assert!(h.is_some()); +// let h = h.unwrap(); +// assert!(!h.is_empty()); +// assert_eq!(h[0].timeout_ms, 1111); +// assert_eq!(h[0].command, "echo i have spawned"); + +// let h = hooks.get(&HookTrigger::UserPromptSubmit); +// assert!(h.is_some()); +// let h = h.unwrap(); +// assert!(!h.is_empty()); +// assert_eq!(h[0].command, "echo user submitted"); +// assert_eq!(h[0].timeout_ms, 1000); + +// let alias = merged.tool_aliases(); +// assert_eq!(alias.len(), 2); +// assert!(alias.contains_key("fs_read")); +// assert!(alias.contains_key("execute_bash")); + +// let tool = merged.get_tool_write(); +// assert!(tool.override_path.contains(&"Cargo.lock".into())); +// assert_eq!(tool.allow.list.len(), 2); +// assert_eq!(tool.override_path.len(), 1); +// assert_eq!(tool.deny.list.len(), 1); + +// let tool = merged.get_tool_read(); +// assert_eq!(tool.allow.list.len(), 2); +// assert_eq!(tool.override_path.len(), 0); +// assert_eq!(tool.deny.list.len(), 1); + +// let tool = merged.get_tool_shell(); +// assert_eq!(tool.allow.list.len(), 2); +// assert_eq!(tool.override_command.len(), 1); +// assert_eq!(tool.deny.list.len(), 1); + +// let tool = merged.get_tool_aws(); +// assert!(tool.allow.list.is_empty()); +// assert!(tool.deny.list.is_empty()); + +// assert_eq!("child", format!("{merged}")); +// assert_eq!("child", format!("{merged:?}")); +// Ok(()) +// } +// } diff --git a/src/config/native.rs b/src/config/native.rs new file mode 100644 index 0000000..75a51b4 --- /dev/null +++ b/src/config/native.rs @@ -0,0 +1,528 @@ +use { + crate::agent::{ + AwsTool as KiroAwsTool, ExecuteShellTool as KiroShellTool, ReadTool as KiroReadTool, + WriteTool as KiroWriteTool, + }, + facet::Facet, + facet_kdl as kdl, + std::{collections::HashSet, fmt::Display}, +}; + +#[derive(Facet, Debug, PartialEq, Clone, Eq, Hash)] +pub struct GenericListItem { + #[facet(kdl::argument)] + item: String, +} +macro_rules! define_tool { + ($name:ident) => { + #[derive(Clone, Debug, Default, PartialEq, Eq)] + pub struct $name { + pub allows: HashSet, + pub denies: HashSet, + pub overrides: HashSet, + pub disable_auto_readonly: Option, + pub deny_by_default: Option, + } + + impl $name { + pub fn merge(mut self, other: Self) -> Self { + self.allows.extend(other.allows); + self.denies.extend(other.denies); + self.disable_auto_readonly = + self.disable_auto_readonly.or(other.disable_auto_readonly); + self.deny_by_default = self.deny_by_default.or(other.deny_by_default); + self + } + } + }; +} + +macro_rules! define_kdl_doc { + ($name:ident) => { + #[derive(Facet, Clone, Debug, Default, PartialEq, Eq)] + #[facet(default, rename_all = "kebab-case")] + pub struct $name { + #[facet(default, kdl::children)] + pub allows: Vec, + #[facet(default, kdl::children)] + pub denies: Vec, + #[facet(default, kdl::children)] + pub overrides: Vec, + #[facet(default, kdl::property)] + pub deny_by_default: Option, + #[facet(default, kdl::property)] + pub disable_auto_readonly: Option, + } + }; +} + +macro_rules! define_tool_into { + ($name:ident, $to:ident) => { + impl From<$name> for $to { + fn from(value: $name) -> $to { + $to { + allows: split_newline(value.allows), + denies: split_newline(value.denies), + overrides: split_newline(value.overrides), + deny_by_default: value.deny_by_default, + disable_auto_readonly: value.disable_auto_readonly, + } + } + } + }; +} + +define_kdl_doc!(AwsToolDoc); +define_kdl_doc!(ExecuteShellToolDoc); +define_kdl_doc!(WriteToolDoc); +define_kdl_doc!(ReadToolDoc); +define_tool!(ExecuteShellTool); +define_tool!(AwsTool); +define_tool!(WriteTool); +define_tool!(ReadTool); +define_tool_into!(ExecuteShellToolDoc, ExecuteShellTool); +define_tool_into!(AwsToolDoc, AwsTool); +define_tool_into!(WriteToolDoc, WriteTool); +define_tool_into!(ReadToolDoc, ReadTool); + +fn split_newline(list: Vec) -> HashSet { + let values: Vec<&str> = list.iter().flat_map(|f| f.item.split("\n")).collect(); + let mut combined: Vec = vec![]; + for v in values { + combined.push(v.to_string()); + } + HashSet::from_iter(combined) +} + +#[derive(Facet, Default, Clone, Debug, PartialEq, Eq)] +#[facet(deny_unknown_fields)] +pub struct NativeToolsDoc { + #[facet(default, kdl::child)] + pub shell: ExecuteShellToolDoc, + #[facet(default, kdl::child)] + pub aws: AwsToolDoc, + #[facet(default, kdl::child)] + pub read: ReadToolDoc, + #[facet(default, kdl::child)] + pub write: WriteToolDoc, +} + +#[derive(Default, Clone, Debug, PartialEq, Eq)] +pub struct NativeTools { + pub shell: ExecuteShellTool, + pub aws: AwsTool, + pub read: ReadTool, + pub write: WriteTool, +} + +#[derive(Facet, Debug, Clone, Default, PartialEq, Eq)] +pub struct GenericList { + #[facet(kdl::arguments)] + pub list: Vec, +} + +impl GenericList { + pub fn into_set(self) -> HashSet { + HashSet::from_iter(self.list) + } +} + +impl From<&'static str> for GenericList { + fn from(value: &'static str) -> Self { + Self { + list: vec![value.to_string()], + } + } +} + +impl FromIterator<&'static str> for GenericList { + fn from_iter>(iter: T) -> Self { + Self { + list: iter.into_iter().map(|f| f.to_string()).collect(), + } + } +} + +impl NativeTools { + pub fn merge(mut self, other: Self) -> Self { + self.shell = self.shell.merge(other.shell); + self.aws = self.aws.merge(other.aws); + self.read = self.read.merge(other.read); + self.write = self.write.merge(other.write); + self + } +} + +impl From<&NativeTools> for KiroAwsTool { + fn from(value: &NativeTools) -> Self { + let aws = &value.aws; + KiroAwsTool { + allowed_services: aws.allows.clone(), + denied_services: aws.denies.clone(), + auto_allow_readonly: aws.disable_auto_readonly.unwrap_or(true), + } + } +} + +impl From<&NativeTools> for KiroWriteTool { + fn from(value: &NativeTools) -> Self { + let write = &value.write; + let mut allow: HashSet = write.allows.clone(); + let mut deny: HashSet = write.denies.clone(); + if !write.overrides.is_empty() { + tracing::trace!( + "Override/Forcing write: {:?}", + write.overrides.iter().collect::>() + ); + for cmd in write.overrides.iter() { + allow.insert(cmd.clone()); + if deny.remove(cmd) { + tracing::trace!("Removed from deny: {cmd}"); + } + } + } + + Self { + allowed_paths: allow, + denied_paths: deny, + } + } +} + +impl From<&NativeTools> for KiroReadTool { + fn from(value: &NativeTools) -> Self { + let read = &value.read; + let mut allow: HashSet = read.allows.clone(); + let mut deny: HashSet = read.denies.clone(); + if !read.overrides.is_empty() { + tracing::trace!( + "Override/Forcing write: {:?}", + read.overrides.iter().collect::>() + ); + for cmd in read.overrides.iter() { + allow.insert(cmd.clone()); + if deny.remove(cmd) { + tracing::trace!("Removed from deny: {cmd}"); + } + } + } + + Self { + allowed_paths: allow, + denied_paths: deny, + } + } +} + +impl From<&NativeTools> for KiroShellTool { + fn from(value: &NativeTools) -> Self { + let shell = &value.shell; + let mut allow: HashSet = shell.allows.clone(); + let mut deny: HashSet = shell.denies.clone(); + + if !shell.overrides.is_empty() { + tracing::trace!( + "Override/Forcing commands: {:?}", + shell.overrides.iter().collect::>() + ); + for cmd in shell.overrides.iter() { + allow.insert(cmd.clone()); + if deny.remove(cmd) { + tracing::trace!("Removed command from deny: {cmd}"); + } + } + } + Self { + allowed_commands: allow, + denied_commands: deny, + deny_by_default: shell.deny_by_default.unwrap_or(false), + auto_allow_readonly: shell.disable_auto_readonly.unwrap_or(true), + } + } +} + +// #[cfg(test)] +// mod tests { +// use {super::*, crate::Result}; + +// #[derive(Facet, Debug)] +// struct NativeToolsDoc { +// #[facet(kdl::child)] +// native: NativeTools, +// } + +// #[test_log::test] +// fn parse_shell_tool() { +// let kdl = r#"native { +// shell deny_by_default=#true disable_auto_readonly=#false { +// allow "ls .*" "git status" +// deny "rm -rf /" +// override "git push" +// } +// }"#; + +// let doc: NativeToolsDoc = facet_kdl::from_str(kdl).unwrap(); +// let shell = doc.native.shell.unwrap(); +// assert_eq!(shell.allow.unwrap().list.len(), 2); +// assert_eq!(shell.deny.unwrap().list.len(), 1); +// assert!(shell.deny_by_default.unwrap()); +// assert!(!shell.disable_auto_readonly.unwrap()); +// assert_eq!(shell.r#override.len(), 1); +// } + +// #[test_log::test] +// fn parse_aws_tool() { +// let kdl = r#"native { +// aws disable_auto_readonly=#true { +// allow "ec2" "s3" +// deny "iam" +// } +// }"#; + +// let doc: NativeToolsDoc = facet_kdl::from_str(kdl).unwrap(); +// let aws = doc.native.aws.unwrap(); +// assert!(aws.disable_auto_readonly.unwrap()); +// assert_eq!(aws.allow.unwrap().list.len(), 2); +// assert_eq!(aws.deny.unwrap().list.len(), 1); +// } + +// #[test_log::test] +// fn parse_read_write_tools() { +// let kdl = r#"native { +// read { +// allow "*.rs" "*.toml" +// deny "/etc/*" +// override "/etc/hosts" +// } +// write { +// allow "*.txt" +// deny "/tmp/*" +// override "/tmp/allowed" +// } +// }"#; + +// let doc: NativeToolsDoc = facet_kdl::from_str(kdl).unwrap(); +// assert_eq!(doc.native.read.unwrap().allow.unwrap().list.len(), 2); +// assert_eq!(doc.native.write.unwrap().allow.unwrap().list.len(), 1); +// } + +// #[test_log::test] +// pub fn test_native_merge_empty() -> Result<()> { +// let child = NativeTools::default(); +// let parent = NativeTools::default(); +// let merged = child.merge(parent); + +// assert_eq!(merged, NativeTools::default()); +// Ok(()) +// } + +// #[test_log::test] +// pub fn test_native_merge_empty_child() -> Result<()> { +// let child = NativeTools::default(); +// let parent = NativeTools { +// aws: Some(AwsTool { +// disable_auto_readonly: None, +// allow: Some(vec!["ec2"].into_iter().collect()), +// deny: Some(vec!["iam"].into_iter().collect()), +// }), +// shell: Some(ExecuteShellTool { +// allow: Some(vec!["ls .*"].into_iter().collect()), +// deny: Some(vec!["git push"].into_iter().collect()), +// r#override: vec![Override::from("rm -rf /")], +// deny_by_default: Some(true), +// disable_auto_readonly: Some(false), +// }), +// read: Some(ReadTool { +// allow: Some(vec!["ls .*"].into_iter().collect()), +// deny: Some(vec!["git push"].into_iter().collect()), +// r#override: vec![Override::from("rm -rf /")], +// }), +// write: Some(WriteTool { +// allow: Some(vec!["ls .*"].into_iter().collect()), +// deny: Some(vec!["git push"].into_iter().collect()), +// overrides: vec![Override::from("rm -rf /")], +// }), +// }; + +// let merged = child.merge(parent.clone()); +// assert_eq!(merged.aws, parent.aws); +// assert_eq!(merged.shell, parent.shell); +// assert_eq!(merged.read, parent.read); +// assert_eq!(merged.write, parent.write); +// Ok(()) +// } + +// #[test_log::test] +// pub fn test_native_merge_child_parent() -> Result<()> { +// let child = NativeTools { +// aws: Some(AwsTool { +// disable_auto_readonly: Some(true), +// allow: Some(vec!["ec2"].into_iter().collect()), +// deny: None, +// }), +// ..Default::default() +// }; + +// let parent = NativeTools { +// aws: Some(AwsTool { +// disable_auto_readonly: None, +// allow: Some(vec!["ec2"].into_iter().collect()), +// deny: Some(vec!["iam"].into_iter().collect()), +// }), +// ..Default::default() +// }; + +// let merged = child.merge(parent); +// let aws = merged.aws.unwrap(); +// assert!(aws.disable_auto_readonly.unwrap()); +// // Should have deduplicated ec2 +// assert_eq!(aws.allow.unwrap().into_set().len(), 1); +// assert_eq!( +// aws.deny.unwrap().into_set(), +// HashSet::from_iter(vec!["iam".to_string()]) +// ); +// Ok(()) +// } + +// #[test_log::test] +// pub fn test_native_merge_shell() -> Result<()> { +// let child = ExecuteShellTool::default(); +// let parent = ExecuteShellTool { +// deny_by_default: Some(false), +// disable_auto_readonly: Some(false), +// ..Default::default() +// }; + +// let merged = child.clone().merge(parent); +// assert!(!merged.deny_by_default.unwrap()); +// assert!(!merged.disable_auto_readonly.unwrap()); + +// let parent = ExecuteShellTool { +// deny_by_default: Some(true), +// disable_auto_readonly: Some(true), +// ..Default::default() +// }; +// let merged = child.clone().merge(parent); +// assert!(merged.deny_by_default.unwrap()); +// assert!(merged.disable_auto_readonly.unwrap()); + +// let child = ExecuteShellTool { +// deny_by_default: Some(false), +// disable_auto_readonly: Some(false), +// ..Default::default() +// }; +// let parent = ExecuteShellTool { +// deny_by_default: Some(true), +// disable_auto_readonly: Some(true), +// ..Default::default() +// }; +// let merged = child.merge(parent); +// assert!(!merged.deny_by_default.unwrap()); +// assert!(!merged.disable_auto_readonly.unwrap()); +// Ok(()) +// } + +// #[test_log::test] +// pub fn test_native_aws_kiro() -> Result<()> { +// let a = NativeTools::default(); +// let kiro = KiroAwsTool::from(&a); +// assert!(kiro.auto_allow_readonly); +// assert!(kiro.allowed_services.is_empty()); +// assert!(kiro.denied_services.is_empty()); + +// let a = NativeTools { +// aws: Some(AwsTool { +// disable_auto_readonly: Some(true), +// allow: Some("blah".into()), +// deny: Some("blahblah".into()), +// }), +// ..Default::default() +// }; + +// let kiro = KiroAwsTool::from(&a); +// assert!(!kiro.auto_allow_readonly); +// assert!(kiro.allowed_services.contains("blah")); +// assert!(kiro.denied_services.contains("blahblah")); +// assert_eq!(kiro.allowed_services.len() + kiro.denied_services.len(), +// 2); Ok(()) +// } + +// #[test_log::test] +// pub fn test_native_shell_kiro() -> Result<()> { +// let a = NativeTools::default(); +// let kiro = KiroShellTool::from(&a); +// assert!(kiro.auto_allow_readonly); +// assert!(kiro.allowed_commands.is_empty()); +// assert!(kiro.denied_commands.is_empty()); + +// let a = NativeTools { +// shell: Some(ExecuteShellTool { +// allow: Some("ls".into()), +// deny: Some("rm".into()), +// deny_by_default: None, +// disable_auto_readonly: None, +// r#override: vec!["rm".into()], +// }), +// ..Default::default() +// }; +// let kiro = KiroShellTool::from(&a); +// assert!(kiro.auto_allow_readonly); +// assert_eq!(kiro.allowed_commands.len(), 2); +// assert_eq!( +// kiro.allowed_commands, +// HashSet::from_iter(vec!["ls".to_string(), "rm".to_string()]) +// ); +// assert!(kiro.denied_commands.is_empty()); +// Ok(()) +// } + +// #[test_log::test] +// pub fn test_native_read_kiro() -> Result<()> { +// let a = NativeTools::default(); +// let kiro = KiroReadTool::from(&a); +// assert!(kiro.allowed_paths.is_empty()); +// assert!(kiro.denied_paths.is_empty()); + +// let a = NativeTools { +// read: Some(ReadTool { +// allow: Some("ls".into()), +// deny: Some("rm".into()), +// r#override: vec!["rm".into()], +// }), +// ..Default::default() +// }; +// let kiro = KiroReadTool::from(&a); +// assert_eq!(kiro.allowed_paths.len(), 2); +// assert_eq!( +// kiro.allowed_paths, +// HashSet::from_iter(vec!["ls".to_string(), "rm".to_string()]) +// ); +// assert!(kiro.denied_paths.is_empty()); +// Ok(()) +// } + +// #[test_log::test] +// pub fn test_native_write_kiro() -> Result<()> { +// let a = NativeTools::default(); +// let kiro = KiroWriteTool::from(&a); +// assert!(kiro.allowed_paths.is_empty()); +// assert!(kiro.denied_paths.is_empty()); + +// let a = NativeTools { +// write: Some(WriteTool { +// allow: Some("ls".into()), +// deny: Some("rm".into()), +// overrides: vec!["rm".into()], +// }), +// ..Default::default() +// }; +// let kiro = KiroWriteTool::from(&a); +// assert_eq!(kiro.allowed_paths.len(), 2); +// assert_eq!( +// kiro.allowed_paths, +// HashSet::from_iter(vec!["ls".to_string(), "rm".to_string()]) +// ); +// assert!(kiro.denied_paths.is_empty()); +// Ok(()) +// } +// } diff --git a/src/generator/discover.rs b/src/generator/discover.rs index 9d2846f..5f1b149 100644 --- a/src/generator/discover.rs +++ b/src/generator/discover.rs @@ -1,7 +1,6 @@ use { super::*, - crate::kdl::{GeneratorConfig, KdlAgent}, - knuffel::parse, + crate::config::{GeneratorConfig, KdlAgent}, std::{fmt::Display, ops::Deref, path::Path}, }; @@ -10,11 +9,12 @@ pub fn load_inline(fs: &Fs, path: impl AsRef) -> Result { let content = fs .read_to_string_sync(&path) .wrap_err_with(|| format!("failed to read path '{}'", path.as_ref().display()))?; - match parse(&format!("{}", path.as_ref().display()), &content) { + + match facet_kdl::from_str(&content) { Ok(c) => Ok(c), Err(e) => { let err_msg = e.to_string(); - eprintln!("{:?}", miette::Report::new(e)); + // eprintln!("{:?}", miette::Report::new(e)); Err(eyre!("failed to parse: {err_msg}")) } } @@ -96,7 +96,7 @@ pub fn discover( let local_path = location.local_kg(); let global_agents: GeneratorConfig = load_inline(fs, global_path)?; let local_agents: GeneratorConfig = load_inline(fs, local_path)?; - tracing::debug!("found {} local agents", local_agents.agents.len()); + tracing::debug!("found {} local agents", local_agents.agent.len()); let local_names = local_agents.names(); let global_names = global_agents.names(); diff --git a/src/generator/merge.rs b/src/generator/merge.rs index 8e8329e..7d3461e 100644 --- a/src/generator/merge.rs +++ b/src/generator/merge.rs @@ -1,4 +1,4 @@ -use {super::*, crate::kdl::KdlAgent, std::collections::HashSet}; +use {super::*, crate::config::KdlAgent, std::collections::HashSet}; impl Generator { /// Resolve transitive inheritance chain for an agent @@ -69,80 +69,89 @@ impl Generator { } } -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - #[test_log::test] - async fn test_merge_inheritance_chain() -> Result<()> { - let fs = Fs::new(); - let generator = Generator::new( - fs, - ConfigLocation::Local, - crate::output::OutputFormat::Table(true), - )?; - - let merged = generator.merge()?; - assert_eq!(merged.len(), 3); - - // Find dependabot agent - let dependabot = merged - .iter() - .find(|a| a.name == "dependabot") - .expect("dependabot agent not found"); - - // Verify inheritance chain was resolved: dependabot -> aws-test -> base - assert_eq!( - dependabot.description, - Some("I make life painful for developers".to_string()) - ); - - // Should have prompt from aws-test - assert_eq!(dependabot.prompt, Some("you are an AWS expert".to_string())); - - // Should have tools from base - let tools = dependabot.tools(); - assert!(tools.contains("*")); - - // Should have allowed_tools merged from base and aws-test - let allowed = dependabot.allowed_tools(); - assert!(allowed.contains("read")); - assert!(allowed.contains("knowledge")); - assert!(allowed.contains("@fetch")); - assert!(allowed.contains("@awsdocs")); - - // Should have resources from all three - let resources: Vec = dependabot.resources().map(|s| s.to_string()).collect(); - assert!(resources.contains(&"file://README.md".to_string())); - assert!(resources.contains(&"file://AGENTS.md".to_string())); - assert!(resources.contains(&"file://.amazonq/rules/**/*.md".to_string())); - - // Should have hooks from all levels - let hooks = dependabot.hooks(); - assert!(hooks.contains_key(&crate::agent::hook::HookTrigger::AgentSpawn)); - - // Should have force permissions from dependabot overriding denies from base - let shell = dependabot.get_tool_shell(); - assert!(shell.override_command.contains(&"git commit .*".into())); - assert!(shell.override_command.contains(&"git push .*".into())); - - let read = dependabot.get_tool_read(); - assert!(read.override_path.contains(&".*Cargo.toml.*".into())); - - let write = dependabot.get_tool_write(); - assert!(write.override_path.contains(&".*Cargo.toml.*".into())); - - // Should have aws tool from aws-test - let aws = dependabot.get_tool_aws(); - assert!(aws.allow.list.contains("ec2")); - assert!(aws.allow.list.contains("s3")); - assert!(aws.deny.list.contains("iam")); - - // check try_from - let results = generator.write_all(true).await?; - assert!(!results.is_empty()); - - Ok(()) - } -} +// #[cfg(test)] +// mod tests { +// use {super::*, serde_yaml2::to_string}; + +// #[tokio::test] +// #[test_log::test] +// async fn test_merge_inheritance_chain() -> Result<()> { +// let fs = Fs::new(); +// let generator = Generator::new( +// fs, +// ConfigLocation::Local, +// crate::output::OutputFormat::Table(true), +// )?; + +// let merged = generator.merge()?; +// assert_eq!(merged.len(), 3); + +// // Find dependabot agent +// let dependabot = merged +// .iter() +// .find(|a| a.name == "dependabot") +// .expect("dependabot agent not found"); + +// // Verify inheritance chain was resolved: dependabot -> aws-test -> +// base assert_eq!(dependabot.description(), "asds"); + +// // Should have prompt from aws-test +// assert_eq!(dependabot.prompt(), "you are an AWS expert".to_string()); + +// // Should have tools from base +// let tools = dependabot.tools(); +// assert!(tools.contains("*")); + +// // Should have allowed_tools merged from base and aws-test +// let allowed = dependabot.allowed_tools(); +// assert!(allowed.contains("read")); +// assert!(allowed.contains("knowledge")); +// assert!(allowed.contains("@fetch")); +// assert!(allowed.contains("@awsdocs")); + +// // Should have resources from all three +// let resources: Vec = dependabot.resources().map(|s| +// s.to_string()).collect(); assert!(resources.contains(&"file://README.md".to_string())); +// assert!(resources.contains(&"file://AGENTS.md".to_string())); +// assert!(resources.contains(&"file://.amazonq/rules/**/*.md".to_string())); + +// // Should have hooks from all levels +// let hooks = dependabot.hooks(); +// assert!(hooks.contains_key(& +// crate::agent::hook::HookTrigger::AgentSpawn)); + +// // Should have force permissions from dependabot overriding denies +// from base let shell = dependabot.get_tool_shell(); +// let overrides = shell.override_commands(); +// assert!(overrides.contains(&"git commit .*".into())); +// assert!(overrides.contains(&"git push .*".into())); + +// let read = dependabot.get_tool_read(); +// let overrides = read.override_paths(); +// assert!(overrides.contains(&".*Cargo.toml.*".into())); + +// let write = dependabot.get_tool_write(); +// let overrides = read.override_paths(); +// assert!(overrides.contains(&".*Cargo.toml.*".into())); + +// // Should have aws tool from aws-test +// let aws = dependabot.get_tool_aws(); + +// assert!( +// aws.allow +// .unwrap_or_default() +// .list +// .iter() +// .any(|i| i == "ec2") +// ); +// assert!(aws.allow.unwrap_or_default().list.iter().any(|i| i == +// "s3")); assert!(aws.deny.unwrap_or_default().list.iter().any(|i| i == +// "iam")); + +// // check try_from +// let results = generator.write_all(true).await?; +// assert!(!results.is_empty()); + +// Ok(()) +// } +// } diff --git a/src/generator/mod.rs b/src/generator/mod.rs index 964bac8..02c55b5 100644 --- a/src/generator/mod.rs +++ b/src/generator/mod.rs @@ -2,7 +2,7 @@ use { crate::{ Result, agent::{Agent, ToolTarget}, - kdl::KdlAgent, + config::KdlAgent, os::Fs, }, color_eyre::eyre::{Context, eyre}, @@ -37,15 +37,15 @@ impl AgentResult { ToolTarget::Read => self .agent .get_tool_read() - .override_path + .override_paths() .iter() .cloned() .map(|f| f.to_string()) .collect(), ToolTarget::Write => self .agent - .get_tool_write() - .override_path + .get_tool_write().overrides +// .override_paths() .iter() .cloned() .map(|f| f.to_string()) @@ -53,7 +53,7 @@ impl AgentResult { ToolTarget::Shell => self .agent .get_tool_shell() - .override_command + .override_commands() .iter() .cloned() .map(|f| f.to_string()) diff --git a/src/kdl/mcp.rs b/src/kdl/mcp.rs index 11d387e..161a80a 100644 --- a/src/kdl/mcp.rs +++ b/src/kdl/mcp.rs @@ -63,21 +63,12 @@ pub struct CustomToolConfigKdl { impl From for CustomToolConfig { fn from(value: CustomToolConfigKdl) -> Self { let command = value.command.unwrap_or_default(); - let oauth = value.oauth.map(|o| crate::agent::OAuthConfig { - redirect_uri: Some(o.redirect_uri), - }); let url = value.url.unwrap_or_default(); Self { url, - r#type: if command.is_empty() { - crate::agent::TransportType::Stdio - } else { - crate::agent::TransportType::Http - }, command, args: value.args.args, - oauth, timeout: if value.timeout == 0 { crate::agent::tool_default_timeout() } else { diff --git a/src/kdl/merge.rs b/src/kdl/merge.rs index 7586fc3..98a54bf 100644 --- a/src/kdl/merge.rs +++ b/src/kdl/merge.rs @@ -35,172 +35,173 @@ impl KdlAgent { } } -#[cfg(test)] -mod tests { - use {super::*, crate::agent::hook::HookTrigger, color_eyre::eyre::eyre, knuffel::parse}; - - #[test_log::test] - fn test_agent_merge() -> crate::Result<()> { - let kdl_agents = r#" - agent "child" template=false { - description "I am a child" - resource "file://child.md" - resource "file://README.md" - inherits "parent" - include-mcp-json true - tools "@awsdocs" "shell" - native-tool { - write { - override "Cargo.lock" - } - shell { - override "git push .*" - } - } - hook { - agent-spawn "spawn" { - command "echo i have spawned" - max-output-size 9000 - cache-ttl-seconds 2 - } - } - alias "execute_bash" "shell" - } - agent "parent" template=true { - description "I am parent" - resource "file://parent.md" - resource "file://README.md" - tools "web_search" "shell" - prompt "i tell you what to do" - model "claude" - allowed-tools "write" - alias "execute_bash" "shell" - alias "fs_read" "read" - native-tool { - read { - allow "./src/*" "./scripts/**" - deny "Cargo.lock" - } - write { - allow "./src/*" "./scripts/**" - deny "Cargo.lock" - } - - shell { - allow "git status .*" "git pull .*" - deny "git push .*" - } - } - hook { - agent-spawn "spawn" { - timeout-ms 1111 - } - user-prompt-submit "submit" { - command "echo user submitted" - timeout-ms 1000 - } - pre-tool-use "pre" { - command "echo before tool" - matcher "git.*" - } - post-tool-use "post" { - command "echo after tool" - } - stop "stop" { - command "echo stopped" - } - } - } - "#; - - let config: GeneratorConfig = match parse("example.kdl", kdl_agents) { - Ok(c) => c, - Err(e) => { - eprintln!("{:?}", miette::Report::new(e)); - return Err(eyre!("failed to parse {kdl_agents}")); - } - }; - assert_eq!(config.agents.len(), 2); - let child = config - .agents - .iter() - .find(|a| a.name == "child") - .unwrap() - .clone(); - let parent = config - .agents - .iter() - .find(|a| a.name == "parent") - .unwrap() - .clone(); - let merged = child.merge(parent); - assert!(merged.description.is_some()); - let d = merged.description.clone().unwrap(); - assert_eq!(d, "I am a child"); - - assert_eq!(merged.resources.len(), 3); - assert!(!merged.is_template()); - assert!(merged.include_mcp_json()); - - assert_eq!(merged.inherits.parents.len(), 1); - assert!(merged.inherits.parents.contains("parent")); - - assert_eq!(merged.prompt, Some("i tell you what to do".to_string())); - let tools = merged.tools(); - assert_eq!(tools.len(), 3); - assert!(tools.contains("@awsdocs")); - assert!(tools.contains("shell")); - assert!(tools.contains("web_search")); - - assert_eq!(merged.model, Some("claude".to_string())); - - let allowed_tools = merged.allowed_tools(); - assert_eq!(allowed_tools.len(), 1); - assert!(allowed_tools.contains("write")); - - let hooks = merged.hooks(); - assert!(!hooks.is_empty()); - let h = hooks.get(&HookTrigger::AgentSpawn); - assert!(h.is_some()); - let h = h.unwrap(); - assert!(!h.is_empty()); - assert_eq!(h[0].timeout_ms, 1111); - assert_eq!(h[0].command, "echo i have spawned"); - - let h = hooks.get(&HookTrigger::UserPromptSubmit); - assert!(h.is_some()); - let h = h.unwrap(); - assert!(!h.is_empty()); - assert_eq!(h[0].command, "echo user submitted"); - assert_eq!(h[0].timeout_ms, 1000); - - let alias = merged.tool_aliases(); - assert_eq!(alias.len(), 2); - assert!(alias.contains_key("fs_read")); - assert!(alias.contains_key("execute_bash")); - - let tool = merged.get_tool_write(); - assert!(tool.override_path.contains(&"Cargo.lock".into())); - assert_eq!(tool.allow.list.len(), 2); - assert_eq!(tool.override_path.len(), 1); - assert_eq!(tool.deny.list.len(), 1); - - let tool = merged.get_tool_read(); - assert_eq!(tool.allow.list.len(), 2); - assert_eq!(tool.override_path.len(), 0); - assert_eq!(tool.deny.list.len(), 1); - - let tool = merged.get_tool_shell(); - assert_eq!(tool.allow.list.len(), 2); - assert_eq!(tool.override_command.len(), 1); - assert_eq!(tool.deny.list.len(), 1); - - let tool = merged.get_tool_aws(); - assert!(tool.allow.list.is_empty()); - assert!(tool.deny.list.is_empty()); - - assert_eq!("child", format!("{merged}")); - assert_eq!("child", format!("{merged:?}")); - Ok(()) - } -} +// #[cfg(test)] +// mod tests { +// use {super::*, crate::agent::hook::HookTrigger, color_eyre::eyre::eyre, +// knuffel::parse}; + +// #[test_log::test] +// fn test_agent_merge() -> crate::Result<()> { +// let kdl_agents = r#" +// agent "child" template=$false { +// description "I am a child" +// resource "file://child.md" +// resource "file://README.md" +// inherits "parent" +// include-mcp-json true +// tools "@awsdocs" "shell" +// native-tool { +// write { +// override "Cargo.lock" +// } +// shell { +// override "git push .*" +// } +// } +// hook { +// agent-spawn "spawn" { +// command "echo i have spawned" +// max-output-size 9000 +// cache-ttl-seconds 2 +// } +// } +// alias "execute_bash" "shell" +// } +// agent "parent" template=#true { +// description "I am parent" +// resource "file://parent.md" +// resource "file://README.md" +// tools "web_search" "shell" +// prompt "i tell you what to do" +// model "claude" +// allowed-tools "write" +// alias "execute_bash" "shell" +// alias "fs_read" "read" +// native-tool { +// read { +// allow "./src/*" "./scripts/**" +// deny "Cargo.lock" +// } +// write { +// allow "./src/*" "./scripts/**" +// deny "Cargo.lock" +// } + +// shell { +// allow "git status .*" "git pull .*" +// deny "git push .*" +// } +// } +// hook { +// agent-spawn "spawn" { +// timeout-ms 1111 +// } +// user-prompt-submit "submit" { +// command "echo user submitted" +// timeout-ms 1000 +// } +// pre-tool-use "pre" { +// command "echo before tool" +// matcher "git.*" +// } +// post-tool-use "post" { +// command "echo after tool" +// } +// stop "stop" { +// command "echo stopped" +// } +// } +// } +// "#; + +// let config: GeneratorConfig = match parse("example.kdl", kdl_agents) +// { Ok(c) => c, +// Err(e) => { +// eprintln!("{:?}", miette::Report::new(e)); +// return Err(eyre!("failed to parse {kdl_agents}")); +// } +// }; +// assert_eq!(config.agents.len(), 2); +// let child = config +// .agents +// .iter() +// .find(|a| a.name == "child") +// .unwrap() +// .clone(); +// let parent = config +// .agents +// .iter() +// .find(|a| a.name == "parent") +// .unwrap() +// .clone(); +// let merged = child.merge(parent); +// assert!(merged.description.is_some()); +// let d = merged.description.clone().unwrap(); +// assert_eq!(d, "I am a child"); + +// assert_eq!(merged.resources.len(), 3); +// assert!(!merged.is_template()); +// assert!(merged.include_mcp_json()); + +// assert_eq!(merged.inherits.parents.len(), 1); +// assert!(merged.inherits.parents.contains("parent")); + +// assert_eq!(merged.prompt, Some("i tell you what to do".to_string())); +// let tools = merged.tools(); +// assert_eq!(tools.len(), 3); +// assert!(tools.contains("@awsdocs")); +// assert!(tools.contains("shell")); +// assert!(tools.contains("web_search")); + +// assert_eq!(merged.model, Some("claude".to_string())); + +// let allowed_tools = merged.allowed_tools(); +// assert_eq!(allowed_tools.len(), 1); +// assert!(allowed_tools.contains("write")); + +// let hooks = merged.hooks(); +// assert!(!hooks.is_empty()); +// let h = hooks.get(&HookTrigger::AgentSpawn); +// assert!(h.is_some()); +// let h = h.unwrap(); +// assert!(!h.is_empty()); +// assert_eq!(h[0].timeout_ms, 1111); +// assert_eq!(h[0].command, "echo i have spawned"); + +// let h = hooks.get(&HookTrigger::UserPromptSubmit); +// assert!(h.is_some()); +// let h = h.unwrap(); +// assert!(!h.is_empty()); +// assert_eq!(h[0].command, "echo user submitted"); +// assert_eq!(h[0].timeout_ms, 1000); + +// let alias = merged.tool_aliases(); +// assert_eq!(alias.len(), 2); +// assert!(alias.contains_key("fs_read")); +// assert!(alias.contains_key("execute_bash")); + +// let tool = merged.get_tool_write(); +// assert!(tool.override_path.contains(&"Cargo.lock".into())); +// assert_eq!(tool.allow.list.len(), 2); +// assert_eq!(tool.override_path.len(), 1); +// assert_eq!(tool.deny.list.len(), 1); + +// let tool = merged.get_tool_read(); +// assert_eq!(tool.allow.list.len(), 2); +// assert_eq!(tool.override_path.len(), 0); +// assert_eq!(tool.deny.list.len(), 1); + +// let tool = merged.get_tool_shell(); +// assert_eq!(tool.allow.list.len(), 2); +// assert_eq!(tool.override_command.len(), 1); +// assert_eq!(tool.deny.list.len(), 1); + +// let tool = merged.get_tool_aws(); +// assert!(tool.allow.list.is_empty()); +// assert!(tool.deny.list.is_empty()); + +// assert_eq!("child", format!("{merged}")); +// assert_eq!("child", format!("{merged:?}")); +// Ok(()) +// } +// } diff --git a/src/kdl/mod.rs b/src/kdl/mod.rs index b3eb23e..7a56a37 100644 --- a/src/kdl/mod.rs +++ b/src/kdl/mod.rs @@ -30,265 +30,263 @@ impl GeneratorConfig { } } -#[cfg(test)] -mod tests { - use { - super::*, - crate::{agent::hook::HookTrigger, kdl::agent_file::KdlAgentFileSource}, - color_eyre::eyre::eyre, - knuffel::parse, - }; +// #[cfg(test)] +// mod tests { +// use { +// super::*, +// crate::{agent::hook::HookTrigger, kdl::agent_file::KdlAgentFileSource}, +// color_eyre::eyre::eyre, +// knuffel::parse, +// }; - #[test_log::test] - fn test_agent_decoding() -> crate::Result<()> { - let kdl_agents = r#" - agent "test" { - inherits "parent" - description "This is a test agent" - prompt "Generate a test prompt" - resource "file://resource.md" - resource "file://README.md" - include-mcp-json true - tools "*" +// #[test_log::test] +// fn test_agent_decoding() -> crate::Result<()> { +// let kdl_agents = r#" +// agent "test" { +// inherits "parent" +// description "This is a test agent" +// prompt "Generate a test prompt" +// resource "file://resource.md" +// resource "file://README.md" +// include-mcp-json true +// tools "*" - allowed-tools "@awsdocs" - hook { - agent-spawn "spawn" { - command "echo i have spawned" - timeout-ms 1000 - max-output-size 9000 - cache-ttl-seconds 2 - } - user-prompt-submit "submit" { - command "echo user submitted" - } - pre-tool-use "pre" { - command "echo before tool" - matcher "git.*" - } - post-tool-use "post" { - command "echo after tool" - } - stop "stop" { - command "echo stopped" - } - } +// allowed-tools "@awsdocs" +// hook { +// agent-spawn "spawn" { +// command "echo i have spawned" +// timeout-ms 1000 +// max-output-size 9000 +// cache-ttl-seconds 2 +// } +// user-prompt-submit "submit" { +// command "echo user submitted" +// } +// pre-tool-use "pre" { +// command "echo before tool" +// matcher "git.*" +// } +// post-tool-use "post" { +// command "echo after tool" +// } +// stop "stop" { +// command "echo stopped" +// } +// } - mcp "awsdocs" { - command "aws-docs" - args "--verbose" "--config=/path" - env "RUST_LOG" "debug" - env "PATH" "/usr/bin" - header "Authorization" "Bearer token" - timeout 5000 - oauth { - redirect-uri "127.0.0.1:7778" - } - } +// mcp "awsdocs" { +// command "aws-docs" +// args "--verbose" "--config=/path" +// env "RUST_LOG" "debug" +// env "PATH" "/usr/bin" +// header "Authorization" "Bearer token" +// timeout 5000 +// oauth { +// redirect-uri "127.0.0.1:7778" +// } +// } - alias "execute_bash" "shell" +// alias "execute_bash" "shell" - native-tool { - write { - allow "./src/*" "./scripts/**" - deny "Cargo.lock" - override "/tmp" - override "/var/log" - } - shell deny-by-default=true { - allow "git status .*" - deny "git push .*" - override "git pull .*" - } - } +// native-tool { +// write { +// allow "./src/*" "./scripts/**" +// deny "Cargo.lock" +// override "/tmp" +// override "/var/log" +// } +// shell deny-by-default=#true { +// allow "git status .*" +// deny "git push .*" +// override "git pull .*" +// } +// } - tool-setting "@git/status" { - json "{ \"git_user\": \"$GIT_USER\" }" - } - } - "#; +// tool-setting "@git/status" { +// json "{ \"git_user\": \"$GIT_USER\" }" +// } +// } +// "#; - let config: GeneratorConfig = match parse("example.kdl", kdl_agents) { - Ok(c) => c, - Err(e) => { - eprintln!("{:?}", miette::Report::new(e)); - return Err(eyre!("failed to parse {kdl_agents}")); - } - }; - assert_eq!(config.agents.len(), 1); - let agent = config.agents[0].clone(); - assert_eq!(agent.name, "test"); - assert!(agent.model.is_none()); - assert!(!agent.is_template()); - let inherits = agent.inherits(); - assert_eq!(inherits.len(), 1); - assert_eq!(inherits.iter().next().unwrap(), "parent"); - assert!(agent.description.is_some()); - assert!(agent.prompt.is_some()); - assert!(agent.include_mcp_json()); - let tools = agent.tools(); - assert_eq!(tools.len(), 1); - assert_eq!(tools.iter().next().unwrap(), "*"); - let resources: Vec = agent.resources().map(|s| s.to_string()).collect(); - assert_eq!(resources.len(), 2); - assert!(resources.contains(&"file://resource.md".to_string())); - assert!(resources.contains(&"file://README.md".to_string())); +// let config: GeneratorConfig = match parse("example.kdl", kdl_agents) { +// Ok(c) => c, +// Err(e) => { +// eprintln!("{:?}", miette::Report::new(e)); +// return Err(eyre!("failed to parse {kdl_agents}")); +// } +// }; +// assert_eq!(config.agents.len(), 1); +// let agent = config.agents[0].clone(); +// assert_eq!(agent.name, "test"); +// assert!(agent.model.is_none()); +// assert!(!agent.is_template()); +// let inherits = agent.inherits(); +// assert_eq!(inherits.len(), 1); +// assert_eq!(inherits.iter().next().unwrap(), "parent"); +// assert!(agent.description.is_some()); +// assert!(agent.prompt.is_some()); +// assert!(agent.include_mcp_json()); +// let tools = agent.tools(); +// assert_eq!(tools.len(), 1); +// assert_eq!(tools.iter().next().unwrap(), "*"); +// let resources: Vec = agent.resources().map(|s| s.to_string()).collect(); +// assert_eq!(resources.len(), 2); +// assert!(resources.contains(&"file://resource.md".to_string())); +// assert!(resources.contains(&"file://README.md".to_string())); - let hooks = agent.hooks(); - assert!(!hooks.is_empty()); - let hook = hooks.get(&HookTrigger::AgentSpawn); - assert!(hook.is_some()); - assert_eq!(hook.unwrap()[0].command, "echo i have spawned"); +// let hooks = agent.hooks(); +// assert!(!hooks.is_empty()); +// let hook = hooks.get(&HookTrigger::AgentSpawn); +// assert!(hook.is_some()); +// assert_eq!(hook.unwrap()[0].command, "echo i have spawned"); - assert!(hooks.contains_key(&HookTrigger::PreToolUse)); - assert!(hooks.contains_key(&HookTrigger::PostToolUse)); - assert!(hooks.contains_key(&HookTrigger::Stop)); - assert!(hooks.contains_key(&HookTrigger::UserPromptSubmit)); +// assert!(hooks.contains_key(&HookTrigger::PreToolUse)); +// assert!(hooks.contains_key(&HookTrigger::PostToolUse)); +// assert!(hooks.contains_key(&HookTrigger::Stop)); +// assert!(hooks.contains_key(&HookTrigger::UserPromptSubmit)); - let allowed = agent.allowed_tools(); - assert_eq!(allowed.len(), 1); - assert_eq!(allowed.iter().next().unwrap(), "@awsdocs"); +// let allowed = agent.allowed_tools(); +// assert_eq!(allowed.len(), 1); +// assert_eq!(allowed.iter().next().unwrap(), "@awsdocs"); - let mcp = agent.mcp_servers(); - assert_eq!(mcp.len(), 1); - assert!(mcp.contains_key("awsdocs")); - let aws_docs = mcp.get("awsdocs").unwrap(); - assert_eq!(aws_docs.command, "aws-docs"); - assert_eq!(aws_docs.args, vec!["--verbose", "--config=/path"]); - assert!(!aws_docs.disabled); - assert_eq!(aws_docs.headers.len(), 1); - assert_eq!(aws_docs.env.len(), 2); - assert_eq!(aws_docs.timeout, 5000); - assert!(aws_docs.oauth.is_some()); +// let mcp = agent.mcp_servers(); +// assert_eq!(mcp.len(), 1); +// assert!(mcp.contains_key("awsdocs")); +// let aws_docs = mcp.get("awsdocs").unwrap(); +// assert_eq!(aws_docs.command, "aws-docs"); +// assert_eq!(aws_docs.args, vec!["--verbose", "--config=/path"]); +// assert!(!aws_docs.disabled); +// assert_eq!(aws_docs.headers.len(), 1); +// assert_eq!(aws_docs.env.len(), 2); +// assert_eq!(aws_docs.timeout, 5000); +// assert_eq!(agent.tool_aliases().len(), 1); - assert_eq!(agent.tool_aliases().len(), 1); +// let extra = agent.extra_tool_settings()?; +// assert_eq!(extra.len(), 1); +// assert!(extra.contains_key("@git/status")); +// let git_status = extra.get("@git/status").unwrap(); +// assert!(git_status.is_object()); +// assert_eq!(git_status["git_user"], "$GIT_USER"); - let extra = agent.extra_tool_settings()?; - assert_eq!(extra.len(), 1); - assert!(extra.contains_key("@git/status")); - let git_status = extra.get("@git/status").unwrap(); - assert!(git_status.is_object()); - assert_eq!(git_status["git_user"], "$GIT_USER"); +// Ok(()) +// } - Ok(()) - } - - #[test_log::test] - fn test_agent_empty() -> crate::Result<()> { - let kdl_agents = r#" - agent "test" template=true { - } - "#; +// #[test_log::test] +// fn test_agent_empty() -> crate::Result<()> { +// let kdl_agents = r#" +// agent "test" template=#true { +// } +// "#; - let config: GeneratorConfig = match parse("example.kdl", kdl_agents) { - Ok(c) => c, - Err(e) => { - eprintln!("{:?}", miette::Report::new(e)); - return Err(eyre!("failed to parse {kdl_agents}")); - } - }; - assert!(!format!("{config:?}").is_empty()); - assert_eq!(config.agents.len(), 1); - let agent = config.agents[0].clone(); - assert_eq!(agent.name, "test"); - assert!(agent.model.is_none()); - assert!(agent.is_template()); +// let config: GeneratorConfig = match parse("example.kdl", kdl_agents) { +// Ok(c) => c, +// Err(e) => { +// eprintln!("{:?}", miette::Report::new(e)); +// return Err(eyre!("failed to parse {kdl_agents}")); +// } +// }; +// assert!(!format!("{config:?}").is_empty()); +// assert_eq!(config.agents.len(), 1); +// let agent = config.agents[0].clone(); +// assert_eq!(agent.name, "test"); +// assert!(agent.model.is_none()); +// assert!(agent.is_template()); - Ok(()) - } +// Ok(()) +// } - #[test_log::test] - fn test_agent_file_source() -> crate::Result<()> { - let kdl_agent_file_source = r#" - description "agent from file" - prompt "Generate a test prompt" - resource "file://resource.md" - resource "file://README.md" - include-mcp-json true - tools "*" +// #[test_log::test] +// fn test_agent_file_source() -> crate::Result<()> { +// let kdl_agent_file_source = r#" +// description "agent from file" +// prompt "Generate a test prompt" +// resource "file://resource.md" +// resource "file://README.md" +// include-mcp-json true +// tools "*" - allowed-tools "@awsdocs" - hook { - agent-spawn "spawn" { - command "echo i have spawned" - timeout-ms 1000 - max-output-size 9000 - cache-ttl-seconds 2 - } - user-prompt-submit "submit" { - command "echo user submitted" - } - pre-tool-use "pre" { - command "echo before tool" - matcher "git.*" - } - post-tool-use "post" { - command "echo after tool" - } - stop "stop" { - command "echo stopped" - } - } +// allowed-tools "@awsdocs" +// hook { +// agent-spawn "spawn" { +// command "echo i have spawned" +// timeout-ms 1000 +// max-output-size 9000 +// cache-ttl-seconds 2 +// } +// user-prompt-submit "submit" { +// command "echo user submitted" +// } +// pre-tool-use "pre" { +// command "echo before tool" +// matcher "git.*" +// } +// post-tool-use "post" { +// command "echo after tool" +// } +// stop "stop" { +// command "echo stopped" +// } +// } - mcp "awsdocs" { - command "aws-docs" - args "--verbose" "--config=/path" - env "RUST_LOG" "debug" - env "PATH" "/usr/bin" - header "Authorization" "Bearer token" - timeout 5000 - oauth { - redirect-uri "127.0.0.1:7778" - } - } +// mcp "awsdocs" { +// command "aws-docs" +// args "--verbose" "--config=/path" +// env "RUST_LOG" "debug" +// env "PATH" "/usr/bin" +// header "Authorization" "Bearer token" +// timeout 5000 +// oauth { +// redirect-uri "127.0.0.1:7778" +// } +// } - alias "execute_bash" "shell" +// alias "execute_bash" "shell" - native-tool { - write { - allow "./src/*" "./scripts/**" - deny "Cargo.lock" - override "/tmp" - override "/var/log" - } - shell deny-by-default=true { - allow "git status .*" - deny "git push .*" - override "git pull .*" - } - } - "#; +// native-tool { +// write { +// allow "./src/*" "./scripts/**" +// deny "Cargo.lock" +// override "/tmp" +// override "/var/log" +// } +// shell deny-by-default=#true { +// allow "git status .*" +// deny "git push .*" +// override "git pull .*" +// } +// } +// "#; - let agent: KdlAgentFileSource = match parse("example.kdl", kdl_agent_file_source) { - Ok(c) => c, - Err(e) => { - eprintln!("{:?}", miette::Report::new(e)); - return Err(eyre!("failed to parse {kdl_agent_file_source}")); - } - }; +// let agent: KdlAgentFileSource = match parse("example.kdl", kdl_agent_file_source) { +// Ok(c) => c, +// Err(e) => { +// eprintln!("{:?}", miette::Report::new(e)); +// return Err(eyre!("failed to parse {kdl_agent_file_source}")); +// } +// }; - assert_eq!(agent.description.unwrap_or_default(), "agent from file"); - Ok(()) - } +// assert_eq!(agent.description.unwrap_or_default(), "agent from file"); +// Ok(()) +// } - #[test_log::test] - fn test_tool_setting_invalid_json() -> crate::Result<()> { - let kdl = r#" - agent "test" { - tool-setting "bad" { - json "{ invalid json }" - } - } - "#; - let config: GeneratorConfig = parse("test.kdl", kdl)?; - let result = config.agents[0].extra_tool_settings(); - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("Failed to parse JSON") - ); - Ok(()) - } -} +// #[test_log::test] +// fn test_tool_setting_invalid_json() -> crate::Result<()> { +// let kdl = r#" +// agent "test" { +// tool-setting "bad" { +// json "{ invalid json }" +// } +// } +// "#; +// let config: GeneratorConfig = parse("test.kdl", kdl)?; +// let result = config.agents[0].extra_tool_settings(); +// assert!(result.is_err()); +// assert!( +// result +// .unwrap_err() +// .to_string() +// .contains("Failed to parse JSON") +// ); +// Ok(()) +// } +// } diff --git a/src/kdl/native.rs b/src/kdl/native.rs deleted file mode 100644 index 6ac395d..0000000 --- a/src/kdl/native.rs +++ /dev/null @@ -1,480 +0,0 @@ -use { - crate::agent::{ - AwsTool as KiroAwsTool, - ExecuteShellTool as KiroShellTool, - ReadTool as KiroReadTool, - WriteTool as KiroWriteTool, - }, - knuffel::Decode, - std::{collections::HashSet, fmt::Display}, -}; - -#[derive(Decode, Debug, Clone, Default, PartialEq, Eq)] -pub struct GenericList { - #[knuffel(arguments)] - pub list: HashSet, -} - -impl From<&'static str> for GenericList { - fn from(value: &'static str) -> Self { - Self { - list: HashSet::from_iter(vec![value.to_string()]), - } - } -} - -impl FromIterator<&'static str> for GenericList { - fn from_iter>(iter: T) -> Self { - Self { - list: HashSet::from_iter(iter.into_iter().map(|f| f.to_string())), - } - } -} - -#[derive(Decode, Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Override { - #[knuffel(argument)] - pub path: String, -} - -impl Display for Override { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.path) - } -} - -impl From<&str> for Override { - fn from(value: &str) -> Self { - Self { - path: value.to_string(), - } - } -} - -#[derive(Decode, Clone, Debug, Default, PartialEq, Eq)] -pub struct WriteTool { - #[knuffel(child, default)] - pub allow: GenericList, - #[knuffel(child, default)] - pub deny: GenericList, - #[knuffel(children(name = "override"))] - pub override_path: HashSet, -} - -impl WriteTool { - fn merge(mut self, other: Self) -> Self { - self.allow.list.extend(other.allow.list); - self.deny.list.extend(other.deny.list); - self.override_path.extend(other.override_path); - self - } -} - -#[derive(Decode, Clone, Debug, Default, PartialEq, Eq)] -pub struct ReadTool { - #[knuffel(child, default)] - pub allow: GenericList, - #[knuffel(child, default)] - pub deny: GenericList, - #[knuffel(children(name = "override"))] - pub override_path: HashSet, -} - -impl ReadTool { - fn merge(mut self, other: Self) -> Self { - self.allow.list.extend(other.allow.list); - self.deny.list.extend(other.deny.list); - self.override_path.extend(other.override_path); - self - } -} - -#[derive(Decode, Debug, Default, Clone, PartialEq, Eq)] -pub struct AwsTool { - #[knuffel(property)] - pub disable_auto_readonly: Option, - #[knuffel(child, default)] - pub allow: GenericList, - #[knuffel(child, default)] - pub deny: GenericList, -} - -impl AwsTool { - fn merge(mut self, other: Self) -> Self { - self.disable_auto_readonly = self.disable_auto_readonly.or(other.disable_auto_readonly); - self.allow.list.extend(other.allow.list); - self.deny.list.extend(other.deny.list); - self - } -} - -#[derive(Decode, Debug, Clone, Default, PartialEq, Eq)] -pub struct ExecuteShellTool { - #[knuffel(child, default)] - pub allow: GenericList, - #[knuffel(child, default)] - pub deny: GenericList, - #[knuffel(property)] - pub deny_by_default: Option, - #[knuffel(property)] - pub disable_auto_readonly: Option, - #[knuffel(children(name = "override"))] - pub override_command: HashSet, -} - -impl ExecuteShellTool { - fn merge(mut self, other: Self) -> Self { - self.allow.list.extend(other.allow.list); - self.deny.list.extend(other.deny.list); - self.deny_by_default = self.deny_by_default.or(other.deny_by_default); - self.disable_auto_readonly = self.disable_auto_readonly.or(other.disable_auto_readonly); - self.override_command.extend(other.override_command); - self - } -} - -#[derive(Decode, Default, Clone, Debug, PartialEq, Eq)] -pub struct NativeTools { - #[knuffel(child, default)] - pub shell: ExecuteShellTool, - #[knuffel(child, default)] - pub aws: AwsTool, - #[knuffel(child, default)] - pub read: ReadTool, - #[knuffel(child, default)] - pub write: WriteTool, -} - -impl NativeTools { - pub fn merge(mut self, other: Self) -> Self { - self.shell = self.shell.merge(other.shell); - self.aws = self.aws.merge(other.aws); - self.read = self.read.merge(other.read); - self.write = self.write.merge(other.write); - self - } -} - -impl From<&NativeTools> for KiroAwsTool { - fn from(value: &NativeTools) -> Self { - KiroAwsTool { - allowed_services: HashSet::from_iter(value.aws.allow.list.iter().cloned()), - denied_services: HashSet::from_iter(value.aws.deny.list.iter().cloned()), - auto_allow_readonly: match value.aws.disable_auto_readonly { - None => true, - Some(f) => !f, - }, - } - } -} - -impl From<&NativeTools> for KiroWriteTool { - fn from(value: &NativeTools) -> Self { - let mut allow: HashSet = HashSet::from_iter(value.write.allow.list.iter().cloned()); - let mut deny: HashSet = HashSet::from_iter(value.write.deny.list.iter().cloned()); - if !value.write.override_path.is_empty() { - tracing::trace!( - "Override/Forcing write: {:?}", - value.shell.override_command.iter().collect::>() - ); - for cmd in value.write.override_path.iter() { - allow.insert(cmd.path.clone()); - if deny.remove(&cmd.path) { - tracing::trace!("Removed from deny: {cmd}"); - } - } - } - KiroWriteTool { - allowed_paths: allow, - denied_paths: deny, - } - } -} - -impl From<&NativeTools> for KiroReadTool { - fn from(value: &NativeTools) -> Self { - let mut allow: HashSet = HashSet::from_iter(value.read.allow.list.iter().cloned()); - let mut deny: HashSet = HashSet::from_iter(value.read.deny.list.iter().cloned()); - if !value.read.override_path.is_empty() { - tracing::trace!( - "Override/Forcing read: {:?}", - value.shell.override_command.iter().collect::>() - ); - for cmd in value.read.override_path.iter() { - allow.insert(cmd.path.clone()); - if deny.remove(&cmd.path) { - tracing::trace!("Removed from deny: {cmd}"); - } - } - } - KiroReadTool { - allowed_paths: allow, - denied_paths: deny, - } - } -} - -impl From<&NativeTools> for KiroShellTool { - fn from(value: &NativeTools) -> Self { - let mut allow: HashSet = HashSet::from_iter(value.shell.allow.list.iter().cloned()); - let mut deny: HashSet = HashSet::from_iter(value.shell.deny.list.iter().cloned()); - if !value.shell.override_command.is_empty() { - tracing::trace!( - "Override/Forcing commands: {:?}", - value.shell.override_command.iter().collect::>() - ); - for cmd in value.shell.override_command.iter() { - allow.insert(cmd.path.clone()); - if deny.remove(&cmd.path) { - tracing::trace!("Removed command from deny: {cmd}"); - } - } - } - - KiroShellTool { - allowed_commands: allow, - denied_commands: deny, - deny_by_default: value.shell.deny_by_default.unwrap_or(false), - auto_allow_readonly: !(value.shell.disable_auto_readonly.unwrap_or(false)), - } - } -} - -#[cfg(test)] -mod tests { - use {super::*, crate::Result}; - - #[test_log::test] - pub fn test_native_merge_empty() -> Result<()> { - let child = NativeTools::default(); - let parent = NativeTools::default(); - let merged = child.merge(parent); - - assert_eq!(merged, NativeTools::default()); - - Ok(()) - } - - #[test_log::test] - pub fn test_native_merge_empty_child() -> Result<()> { - let child = NativeTools::default(); - let mut parent = NativeTools::default(); - let aws = AwsTool { - disable_auto_readonly: None, - allow: vec!["ec2"].into_iter().collect(), - deny: vec!["iam"].into_iter().collect(), - }; - - let shell = ExecuteShellTool { - allow: vec!["ls .*"].into_iter().collect(), - deny: vec!["git push"].into_iter().collect(), - override_command: HashSet::from_iter(vec![Override::from("rm -rf /")]), - deny_by_default: Some(true), - disable_auto_readonly: Some(false), - }; - - let read = ReadTool { - allow: vec!["ls .*"].into_iter().collect(), - deny: vec!["git push"].into_iter().collect(), - override_path: HashSet::from_iter(vec![Override::from("rm -rf /")]), - }; - let write = WriteTool { - allow: vec!["ls .*"].into_iter().collect(), - deny: vec!["git push"].into_iter().collect(), - override_path: HashSet::from_iter(vec![Override::from("rm -rf /")]), - }; - parent.aws = aws.clone(); - parent.shell = shell.clone(); - parent.read = read.clone(); - parent.write = write.clone(); - let merged = child.merge(parent); - assert_eq!(merged.aws, aws); - assert_eq!(merged.shell, shell); - assert_eq!(merged.read, read); - assert_eq!(merged.write, write); - Ok(()) - } - - #[test_log::test] - pub fn test_native_merge_child_parent() -> Result<()> { - let mut child = NativeTools::default(); - let parent = NativeTools { - aws: AwsTool { - disable_auto_readonly: None, - allow: vec!["ec2"].into_iter().collect(), - deny: vec!["iam"].into_iter().collect(), - }, - shell: ExecuteShellTool { - allow: vec!["ls .*"].into_iter().collect(), - deny: vec!["git push"].into_iter().collect(), - override_command: HashSet::from_iter(vec![Override::from("rm -rf /")]), - deny_by_default: Some(true), - disable_auto_readonly: Some(false), - }, - - read: ReadTool { - allow: vec!["ls .*"].into_iter().collect(), - deny: vec!["git push"].into_iter().collect(), - override_path: HashSet::from_iter(vec![Override::from("rm -rf /")]), - }, - write: WriteTool { - allow: vec!["ls .*"].into_iter().collect(), - deny: vec!["git push"].into_iter().collect(), - override_path: HashSet::from_iter(vec![Override::from("rm -rf /")]), - }, - }; - - child.aws = AwsTool { - disable_auto_readonly: Some(true), - allow: vec!["ec2"].into_iter().collect(), - ..Default::default() - }; - let merged = child.merge(parent); - assert_eq!(merged.aws.allow, vec!["ec2"].into_iter().collect()); - assert_eq!(merged.aws.deny, vec!["iam"].into_iter().collect()); - assert!(merged.aws.disable_auto_readonly.unwrap_or_default()); - Ok(()) - } - - #[test_log::test] - pub fn test_native_merge_shell() -> Result<()> { - let child = ExecuteShellTool::default(); - let parent = ExecuteShellTool { - deny_by_default: Some(false), - disable_auto_readonly: Some(false), - ..Default::default() - }; - - let merged = child.clone().merge(parent); - assert!(!merged.deny_by_default.unwrap_or_default()); - assert!(!merged.disable_auto_readonly.unwrap_or_default()); - - let parent = ExecuteShellTool { - deny_by_default: Some(true), - disable_auto_readonly: Some(true), - ..Default::default() - }; - let merged = child.clone().merge(parent); - assert!(merged.deny_by_default.unwrap_or_default()); - assert!(merged.disable_auto_readonly.unwrap_or_default()); - - let child = ExecuteShellTool { - deny_by_default: Some(false), - disable_auto_readonly: Some(false), - ..Default::default() - }; - let parent = ExecuteShellTool { - deny_by_default: Some(true), - disable_auto_readonly: Some(true), - ..Default::default() - }; - let merged = child.merge(parent); - assert!(!merged.deny_by_default.unwrap_or_default()); - assert!(!merged.disable_auto_readonly.unwrap_or_default()); - Ok(()) - } - - #[test_log::test] - pub fn test_native_aws_kiro() -> Result<()> { - let a = NativeTools::default(); - let kiro = KiroAwsTool::from(&a); - assert!(kiro.auto_allow_readonly); - assert!(kiro.allowed_services.is_empty()); - assert!(kiro.denied_services.is_empty()); - - let a = NativeTools { - aws: AwsTool { - disable_auto_readonly: Some(true), - allow: "blah".into(), - deny: "blahblah".into(), - }, - ..Default::default() - }; - - let kiro = KiroAwsTool::from(&a); - assert!(!kiro.auto_allow_readonly); - assert!(kiro.allowed_services.contains("blah")); - assert!(kiro.denied_services.contains("blahblah")); - assert_eq!(kiro.allowed_services.len() + kiro.denied_services.len(), 2); - Ok(()) - } - - #[test_log::test] - pub fn test_native_shell_kiro() -> Result<()> { - let a = NativeTools::default(); - let kiro = KiroShellTool::from(&a); - assert!(kiro.auto_allow_readonly); - assert!(kiro.allowed_commands.is_empty()); - assert!(kiro.denied_commands.is_empty()); - - let a = NativeTools { - shell: ExecuteShellTool { - allow: "ls".into(), - deny: "rm".into(), - deny_by_default: None, - disable_auto_readonly: None, - override_command: HashSet::from_iter(vec!["rm".into()]), - }, - ..Default::default() - }; - let kiro = KiroShellTool::from(&a); - assert!(kiro.auto_allow_readonly); - assert_eq!(kiro.allowed_commands.len(), 2); - assert_eq!( - kiro.allowed_commands, - HashSet::from_iter(vec!["ls".to_string(), "rm".to_string()]) - ); - assert!(kiro.denied_commands.is_empty()); - Ok(()) - } - - #[test_log::test] - pub fn test_native_read_kiro() -> Result<()> { - let a = NativeTools::default(); - let kiro = KiroReadTool::from(&a); - assert!(kiro.allowed_paths.is_empty()); - assert!(kiro.denied_paths.is_empty()); - - let a = NativeTools { - read: ReadTool { - allow: "ls".into(), - deny: "rm".into(), - override_path: HashSet::from_iter(vec!["rm".into()]), - }, - ..Default::default() - }; - let kiro = KiroReadTool::from(&a); - assert_eq!(kiro.allowed_paths.len(), 2); - assert_eq!( - kiro.allowed_paths, - HashSet::from_iter(vec!["ls".to_string(), "rm".to_string()]) - ); - assert!(kiro.denied_paths.is_empty()); - Ok(()) - } - - #[test_log::test] - pub fn test_native_write_kiro() -> Result<()> { - let a = NativeTools::default(); - let kiro = KiroWriteTool::from(&a); - assert!(kiro.allowed_paths.is_empty()); - assert!(kiro.denied_paths.is_empty()); - - let a = NativeTools { - write: WriteTool { - allow: "ls".into(), - deny: "rm".into(), - override_path: HashSet::from_iter(vec!["rm".into()]), - }, - ..Default::default() - }; - let kiro = KiroWriteTool::from(&a); - assert_eq!(kiro.allowed_paths.len(), 2); - assert_eq!( - kiro.allowed_paths, - HashSet::from_iter(vec!["ls".to_string(), "rm".to_string()]) - ); - assert!(kiro.denied_paths.is_empty()); - Ok(()) - } -} diff --git a/src/main.rs b/src/main.rs index e32b782..e58ec73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ mod agent; mod commands; +mod config; mod generator; -mod kdl; +// mod kdl; mod os; pub mod output; mod schema; From a0555192d142491a3a2659c9c239f306bd32c49b Mon Sep 17 00:00:00 2001 From: dougeEfresh Date: Wed, 31 Dec 2025 18:37:40 +0000 Subject: [PATCH 2/8] refactor --- Cargo.toml | 3 - src/agent/mod.rs | 174 +++++++++++++++++++++----------------- src/config.rs | 29 +++++-- src/config/agent.rs | 155 +++++++++++++++++---------------- src/config/agent_file.rs | 77 +++++++++++++++++ src/config/hook.rs | 71 ++++++++-------- src/config/mcp.rs | 12 +-- src/config/merge.rs | 52 +++++------- src/config/native.rs | 23 +++-- src/generator/discover.rs | 41 ++++----- src/generator/merge.rs | 2 +- src/generator/mod.rs | 22 ++--- src/output.rs | 8 +- 13 files changed, 386 insertions(+), 283 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a6459ba..703c544 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,3 @@ strip = false # Keep symbols for better backtraces debug = 1 # Line numbers only (smaller than full debug=2) lto = "thin" # Faster builds than "fat", still good optimization codegen-units = 16 # Default, balances compile time vs optimization - -[lints.rust] -unused_imports = "allow" diff --git a/src/agent/mod.rs b/src/agent/mod.rs index bcbb666..fd15c4c 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -97,88 +97,104 @@ impl TryFrom<&KdlAgent> for Agent { type Error = color_eyre::Report; fn try_from(value: &KdlAgent) -> std::result::Result { - Ok(Self::default()) - } - - // fn try_from(value: &KdlAgent) -> std::result::Result { - // let native_tools = &value.native_tool; - // let mut tools_settings = HashMap::new(); + let native_tools = &value.native_tool; + let mut tools_settings = HashMap::new(); - // let tool: AwsTool = native_tools.into(); - // let tool_name = ToolTarget::Aws.to_string(); - // if tool != AwsTool::default() { - // tools_settings.insert( - // tool_name.to_string(), - // serde_json::to_value(&tool) - // .map_err(|e| eyre!("Failed to serialize {tool_name} tool - // configuration {e}"))?, ); - // } - // let tool: ReadTool = native_tools.into(); - // let tool_name = ToolTarget::Read.to_string(); - // if tool != ReadTool::default() { - // tools_settings.insert( - // tool_name.to_string(), - // serde_json::to_value(&tool) - // .map_err(|e| eyre!("Failed to serialize {tool_name} tool - // configuration {e}"))?, ); - // } - // let tool: WriteTool = native_tools.into(); - // let tool_name = ToolTarget::Write.to_string(); - // if tool != WriteTool::default() { - // tools_settings.insert( - // tool_name.to_string(), - // serde_json::to_value(&tool) - // .map_err(|e| eyre!("Failed to serialize {tool_name} tool - // configuration {e}"))?, ); - // } - // let tool: ExecuteShellTool = native_tools.into(); - // let tool_name = ToolTarget::Shell.to_string(); - // if tool != ExecuteShellTool::default() { - // tools_settings.insert( - // tool_name.to_string(), - // serde_json::to_value(&tool) - // .map_err(|e| eyre!("Failed to serialize {tool_name} tool - // configuration {e}"))?, ); - // } - // let default_agent = Self::default(); - // let tools = value.tools().clone(); - // let allowed_tools = value.allowed_tools().clone(); - // let resources: HashSet = value.resources().map(|s| - // s.to_string()).collect(); + let tool: AwsTool = native_tools.into(); + let tool_name = ToolTarget::Aws.to_string(); + if tool != AwsTool::default() { + tools_settings.insert( + tool_name.to_string(), + serde_json::to_value(&tool).map_err(|e| { + eyre!( + "Failed to serialize {tool_name} tool + configuration {e}" + ) + })?, + ); + } + let tool: ReadTool = native_tools.into(); + let tool_name = ToolTarget::Read.to_string(); + if tool != ReadTool::default() { + tools_settings.insert( + tool_name.to_string(), + serde_json::to_value(&tool).map_err(|e| { + eyre!( + "Failed to serialize {tool_name} tool + configuration {e}" + ) + })?, + ); + } + let tool: WriteTool = native_tools.into(); + let tool_name = ToolTarget::Write.to_string(); + if tool != WriteTool::default() { + tools_settings.insert( + tool_name.to_string(), + serde_json::to_value(&tool).map_err(|e| { + eyre!( + "Failed to serialize {tool_name} tool + configuration {e}" + ) + })?, + ); + } + let tool: ExecuteShellTool = native_tools.into(); + let tool_name = ToolTarget::Shell.to_string(); + if tool != ExecuteShellTool::default() { + tools_settings.insert( + tool_name.to_string(), + serde_json::to_value(&tool).map_err(|e| { + eyre!( + "Failed to serialize {tool_name} tool + configuration {e}" + ) + })?, + ); + } + let default_agent = Self::default(); + let tools = value.tools.clone(); + let allowed_tools = value.allowed_tools.clone(); + let resources: HashSet = value.resources.clone(); - // // Extra tool settings override native tools - // let extra_tool_settings = value.extra_tool_settings()?; - // tools_settings.extend(extra_tool_settings); + // Extra tool settings override native tools + // let extra_tool_settings = value.extra_tool_settings()?; + // tools_settings.extend(extra_tool_settings); - // Ok(Self { - // name: value.name.clone(), - // description: value.description.clone(), - // prompt: value.prompt.clone(), - // mcp_servers: McpServerConfig { - // mcp_servers: value.mcp_servers(), - // }, - // tools: if tools.is_empty() { - // default_agent.tools - // } else { - // tools - // }, - // tool_aliases: value.tool_aliases(), - // allowed_tools: if allowed_tools.is_empty() { - // default_agent.allowed_tools - // } else { - // allowed_tools - // }, - // resources: if resources.is_empty() { - // default_agent.resources - // } else { - // resources - // }, - // hooks: value.hooks(), - // tools_settings, - // model: value.model.clone(), - // include_mcp_json: value.include_mcp_json(), - // }) - // } + let mut hooks: HashMap> = HashMap::new(); + let triggers: Vec = enum_iterator::all::().collect(); + for t in triggers { + hooks.insert(t, value.hook.hooks(&t)); + } + Ok(Self { + name: value.name.clone(), + description: value.description.clone(), + prompt: value.prompt.clone(), + mcp_servers: McpServerConfig { + mcp_servers: value.mcp.clone(), + }, + tools: if tools.is_empty() { + default_agent.tools + } else { + tools + }, + tool_aliases: value.alias.clone(), + allowed_tools: if allowed_tools.is_empty() { + default_agent.allowed_tools + } else { + allowed_tools + }, + resources: if resources.is_empty() { + default_agent.resources + } else { + resources + }, + hooks, + tools_settings, + model: value.model.clone(), + include_mcp_json: value.include_mcp_json.is_some_and(|f| f), + }) + } } impl Default for Agent { diff --git a/src/config.rs b/src/config.rs index cedf093..89e8f9c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,14 +5,29 @@ mod mcp; mod merge; mod native; -use std::{collections::HashSet, fmt::Debug}; +use std::{collections::HashMap, fmt::Debug}; -pub use {agent::KdlAgent, hook::HookPart, mcp::CustomToolConfigKdl, native::NativeTools}; +pub use agent::{KdlAgent, KdlAgentDoc}; #[derive(facet::Facet, Default)] -pub struct GeneratorConfig { +pub struct GeneratorConfigDoc { #[facet(facet_kdl::children, default)] - pub agent: Vec, + pub agent: Vec, +} + +#[derive(Default)] +pub struct GeneratorConfig { + pub agent: HashMap, +} + +impl From for GeneratorConfig { + fn from(value: GeneratorConfigDoc) -> Self { + let mut agent: HashMap = HashMap::with_capacity(value.agent.len()); + for a in value.agent { + agent.insert(a.name.clone(), a.into()); + } + Self { agent } + } } impl Debug for GeneratorConfig { @@ -22,11 +37,7 @@ impl Debug for GeneratorConfig { } impl GeneratorConfig { - pub fn names(&self) -> HashSet { - self.agent.iter().map(|a| a.name.clone()).collect() - } - pub fn get(&self, name: impl AsRef) -> Option<&KdlAgent> { - self.agent.iter().find(|a| a.name.eq(name.as_ref())) + self.agent.get(name.as_ref()) } } diff --git a/src/config/agent.rs b/src/config/agent.rs index b475228..7a037b8 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -1,13 +1,10 @@ use { super::{ - hook::HookDoc, - mcp::CustomToolConfigKdl, - native::{AwsTool, ExecuteShellTool, NativeTools, NativeToolsDoc, ReadTool, WriteTool}, - }, - crate::agent::{ - CustomToolConfig, OriginalToolName, - hook::{Hook, HookTrigger}, + hook::{HookDoc, HookPart}, + mcp::CustomToolConfigDoc, + native::{NativeTools, NativeToolsDoc}, }, + crate::agent::{CustomToolConfig, OriginalToolName}, color_eyre::eyre::WrapErr, facet::Facet, facet_kdl as kdl, @@ -78,6 +75,7 @@ struct Json { } impl ToolSetting { + #[allow(dead_code)] fn to_value(&self) -> crate::Result<(String, serde_json::Value)> { let v: serde_json::Value = serde_json::from_str(&self.json.value) .wrap_err_with(|| format!("Failed to parse JSON for tool-setting '{}'", self.name))?; @@ -94,7 +92,27 @@ impl ToolSetting { } } +#[derive(Clone, Default)] +pub struct KdlAgent { + pub name: String, + pub template: Option, + pub description: Option, + pub inherits: HashSet, + pub prompt: Option, + pub resources: HashSet, + pub include_mcp_json: Option, + pub tools: HashSet, + pub allowed_tools: HashSet, + pub model: Option, + pub hook: HookPart, + pub mcp: HashMap, + pub alias: HashMap, + pub native_tool: NativeTools, + pub tool_setting: Vec, +} + #[derive(Facet, Clone, Default)] +#[facet(rename = "kabel-case", default)] pub struct KdlAgentDoc { #[facet(kdl::argument)] pub name: String, @@ -108,8 +126,8 @@ pub struct KdlAgentDoc { pub prompt: Option, #[facet(kdl::children, default)] pub(super) resources: Vec, - #[facet(kdl::child, default)] - pub include_mcp_json: Option, + #[facet(kdl::property, default)] + pub include_mcp_json: Option, #[facet(kdl::child, default)] pub(super) tools: Option, #[facet(kdl::child, default)] @@ -119,11 +137,11 @@ pub struct KdlAgentDoc { #[facet(kdl::child, default)] pub(super) hook: Option, #[facet(kdl::children, default)] - pub(super) mcp: Vec, + pub(super) mcp: Vec, #[facet(kdl::children, default)] pub(super) alias: Vec, #[facet(kdl::child, default)] - pub native_tool: Option, + pub native_tool: NativeToolsDoc, #[facet(kdl::children, default)] pub(super) tool_setting: Vec, } @@ -135,19 +153,13 @@ pub struct Description { } #[derive(Facet, Clone, Default, Debug)] -struct Prompt { +pub struct Prompt { #[facet(kdl::argument)] value: String, } #[derive(Facet, Clone, Debug)] -struct IncludeMcpJson { - #[facet(kdl::argument)] - value: bool, -} - -#[derive(Facet, Clone, Debug)] -struct Model { +pub struct Model { #[facet(kdl::argument)] value: String, } @@ -164,7 +176,41 @@ impl Display for KdlAgent { } } +impl From for KdlAgent { + fn from(value: KdlAgentDoc) -> Self { + Self { + name: value.name.clone(), + description: value.description.as_ref().map(|f| f.value.clone()), + prompt: value.prompt.as_ref().map(|f| f.value.clone()), + alias: value.tool_aliases(), + allowed_tools: value.allowed_tools(), + inherits: value.inherits(), + template: value.template, + include_mcp_json: value.include_mcp_json, + hook: value.hooks(), + resources: value.resources(), + model: value.model.as_ref().map(|f| f.value.clone()), + mcp: value.mcp_servers(), + tools: value.tools(), + tool_setting: Default::default(), // TODO use facet::Value + native_tool: value.native_tool.into(), + } + } +} + impl KdlAgent { + pub fn new(name: String) -> Self { + Self { + name, + ..Default::default() + } + } + + pub fn is_template(&self) -> bool { + self.template.is_some_and(|f| f) + } +} +impl KdlAgentDoc { pub fn prompt(&self) -> String { self.prompt.clone().unwrap_or_default().value } @@ -184,41 +230,6 @@ impl KdlAgent { self.template.is_some_and(|f| f) } - pub fn include_mcp_json(&self) -> bool { - self.include_mcp_json - .as_ref() - .map(|i| i.value) - .unwrap_or(false) - } - - pub fn get_tool_aws(&self) -> AwsTool { - self.native_tool - .as_ref() - .and_then(|n| n.aws.clone()) - .unwrap_or_default() - } - - pub fn get_tool_read(&self) -> ReadTool { - self.native_tool - .as_ref() - .and_then(|n| n.read.clone()) - .unwrap_or_default() - } - - pub fn get_tool_write(&self) -> WriteTool { - self.native_tool - .as_ref() - .and_then(|n| n.write.clone()) - .unwrap_or_default() - } - - pub fn get_tool_shell(&self) -> ExecuteShellTool { - self.native_tool - .as_ref() - .and_then(|n| n.shell.clone()) - .unwrap_or_default() - } - pub fn tool_aliases(&self) -> HashMap { self.alias .iter() @@ -226,11 +237,8 @@ impl KdlAgent { .collect() } - pub fn hooks(&self) -> HashMap> { - match &self.hook { - None => HashMap::new(), - Some(h) => h.triggers(), - } + pub fn hooks(&self) -> HookPart { + HookPart::from(self.hook.clone().unwrap_or_default()) } pub fn allowed_tools(&self) -> HashSet { @@ -251,8 +259,8 @@ impl KdlAgent { HashSet::from_iter(self.inherits.parents.clone()) } - pub fn resources(&self) -> impl Iterator { - self.resources.iter().map(|r| r.location.as_str()) + pub fn resources(&self) -> HashSet { + HashSet::from_iter(self.resources.iter().map(|r| r.location.clone())) } pub fn mcp_servers(&self) -> HashMap { @@ -263,18 +271,17 @@ impl KdlAgent { } pub fn extra_tool_settings(&self) -> crate::Result> { - let mut result = HashMap::new(); - for setting in &self.tool_setting { - let (name, value) = setting.to_value()?; - if result.contains_key(&name) { - return Err(color_eyre::eyre::eyre!( - "[{self}] - Duplicate tool-setting '{}' found. Each tool-setting name must be \ - unique.", - name - )); - } - result.insert(name, value); - } - Ok(result) + Ok(HashMap::new()) + // for setting in &self.tool_setting { + // let (name, value) = setting.to_value()?; + // if result.contains_key(&name) { + // return Err(color_eyre::eyre::eyre!( + // "[{self}] - Duplicate tool-setting '{}' found. Each + // tool-setting name must be \ unique.", + // name + // )); + // } + // result.insert(name, value); + // } } } diff --git a/src/config/agent_file.rs b/src/config/agent_file.rs index 8b13789..3a88375 100644 --- a/src/config/agent_file.rs +++ b/src/config/agent_file.rs @@ -1 +1,78 @@ +use { + super::{agent::*, hook::HookDoc, mcp::CustomToolConfigDoc, native::NativeToolsDoc}, + crate::Fs, + color_eyre::eyre::eyre, + facet::Facet, + facet_kdl as kdl, + miette::IntoDiagnostic, + std::path::Path, +}; +#[derive(Facet, Clone, Default)] +#[facet(rename = "kabel-case", default)] +pub struct KdlAgentFileDoc { + #[facet(kdl::child, default)] + pub description: Option, + #[facet(kdl::child, default)] + pub(super) inherits: Inherits, + #[facet(kdl::child, default)] + pub(super) prompt: Option, + #[facet(kdl::children, default)] + pub(super) resources: Vec, + #[facet(kdl::property, default)] + pub include_mcp_json: Option, + #[facet(kdl::child, default)] + pub(super) tools: Option, + #[facet(kdl::child, default)] + pub(super) allowed_tools: Option, + #[facet(kdl::child, default)] + pub(super) model: Option, + #[facet(kdl::child, default)] + pub(super) hook: Option, + #[facet(kdl::children, default)] + pub(super) mcp: Vec, + #[facet(kdl::children, default)] + pub(super) alias: Vec, + #[facet(kdl::child, default)] + pub native_tool: NativeToolsDoc, + #[facet(kdl::children, default)] + pub(super) tool_setting: Vec, +} + +impl KdlAgentDoc { + pub fn from_path( + fs: &Fs, + name: impl AsRef, + path: impl AsRef, + ) -> crate::Result> { + if !fs.exists(&path) { + return Ok(None); + } + + let content = fs.read_to_string_sync(&path)?; + let agent: KdlAgentFileDoc = kdl::from_str(&content) + .into_diagnostic() + .map_err(|e| eyre!("failed {} {e}", path.as_ref().display()))?; + Ok(Some(Self::from_file_source(name, agent))) + } + + pub fn from_file_source(name: impl AsRef, file_source: KdlAgentFileDoc) -> Self { + Self { + name: name.as_ref().to_string(), + description: file_source.description, + template: None, + inherits: Inherits::default(), + prompt: file_source.prompt, + resources: file_source.resources, + include_mcp_json: file_source.include_mcp_json, + tools: file_source.tools, + allowed_tools: file_source.allowed_tools, + model: file_source.model, + hook: file_source.hook, + mcp: file_source.mcp, + alias: file_source.alias, + native_tool: file_source.native_tool, + tool_setting: file_source.tool_setting, + } + } +} diff --git a/src/config/hook.rs b/src/config/hook.rs index fc4eedb..dfe8777 100644 --- a/src/config/hook.rs +++ b/src/config/hook.rs @@ -67,21 +67,6 @@ macro_rules! define_hook_doc { }; } -macro_rules! define_hook { - ($name:ident) => { - #[derive(Default, Clone, Debug, PartialEq, Eq)] - pub struct $name { - #[facet(kdl::argument)] - pub name: String, - command: String, - timeout_ms: u64, - max_output_size: u64, - cache_ttl_seconds: u64, - matcher: Option, - } - }; -} - #[derive(Facet, Clone, Default, Debug, PartialEq, Eq)] #[facet(default)] struct GenericValue { @@ -157,33 +142,47 @@ impl From for HookPart { } } } + impl HookPart { + pub fn hooks(&self, trigger: &HookTrigger) -> Vec { + match trigger { + HookTrigger::AgentSpawn => self.agent_spawn.values().cloned().collect(), + HookTrigger::UserPromptSubmit => self.user_prompt_submit.values().cloned().collect(), + HookTrigger::PreToolUse => self.pre_tool_use.values().cloned().collect(), + HookTrigger::PostToolUse => self.post_tool_use.values().cloned().collect(), + HookTrigger::Stop => self.stop.values().cloned().collect(), + } + } + pub fn merge(mut self, other: Self) -> Self { - match (self.agent_spawn.is_empty(), other.agent_spawn.is_empty()) { - (false, false) => { - let mut hooks = HashMap::with_capacity(self.agent_spawn.len()); - for (k, h) in self.agent_spawn { - if let Some(o) = other.agent_spawn.get(&k) { - hooks.insert(k.to_string(), h.merge(o.clone())); - } else { - hooks.insert(k, h); - } - } - self.agent_spawn = hooks; - for o in other.agent_spawn.keys() { - if !self.agent_spawn.contains_key(o) { - self.agent_spawn - .insert(o.to_string(), other.agent_spawn.get(o).unwrap().clone()); - } - } - } - (true, false) => self.agent_spawn = other.agent_spawn, - _ => {} - }; + self.agent_spawn = merge_hooks(self.agent_spawn, other.agent_spawn); + self.user_prompt_submit = merge_hooks(self.user_prompt_submit, other.user_prompt_submit); + self.pre_tool_use = merge_hooks(self.pre_tool_use, other.pre_tool_use); + self.post_tool_use = merge_hooks(self.post_tool_use, other.post_tool_use); + self.stop = merge_hooks(self.stop, other.stop); self } } +fn merge_hooks( + mut base: HashMap, + other: HashMap, +) -> HashMap { + if base.is_empty() { + return other; + } + if other.is_empty() { + return base; + } + + for (key, other_hook) in other { + base.entry(key) + .and_modify(|h| *h = h.clone().merge(other_hook.clone())) + .or_insert(other_hook); + } + base +} + // #[cfg(test)] // mod tests { // use {super::*, crate::Result, std::time::Duration}; diff --git a/src/config/mcp.rs b/src/config/mcp.rs index fa0f182..669ba44 100644 --- a/src/config/mcp.rs +++ b/src/config/mcp.rs @@ -29,7 +29,7 @@ pub struct RedirectUri { } #[derive(Facet, Clone, Debug)] -pub struct CustomToolConfigKdl { +pub struct CustomToolConfigDoc { #[facet(kdl::argument)] pub name: String, @@ -79,8 +79,8 @@ pub struct Disabled { pub value: bool, } -impl From for CustomToolConfig { - fn from(value: CustomToolConfigKdl) -> Self { +impl From for CustomToolConfig { + fn from(value: CustomToolConfigDoc) -> Self { let command = value.command.map(|c| c.value).unwrap_or_default(); let url = value.url.map(|u| u.value).unwrap_or_default(); @@ -100,8 +100,8 @@ impl From for CustomToolConfig { } } -impl From<&CustomToolConfigKdl> for CustomToolConfig { - fn from(value: &CustomToolConfigKdl) -> Self { +impl From<&CustomToolConfigDoc> for CustomToolConfig { + fn from(value: &CustomToolConfigDoc) -> Self { value.clone().into() } } @@ -113,7 +113,7 @@ mod tests { #[derive(Facet, Debug)] struct McpDoc { #[facet(kdl::child)] - mcp: CustomToolConfigKdl, + mcp: CustomToolConfigDoc, } #[test] diff --git a/src/config/merge.rs b/src/config/merge.rs index d576a2a..8068819 100644 --- a/src/config/merge.rs +++ b/src/config/merge.rs @@ -2,38 +2,26 @@ use super::*; impl KdlAgent { pub fn merge(mut self, other: KdlAgent) -> Self { - Self::default() - // // Child wins for explicit values - // self.include_mcp_json = - // self.include_mcp_json.or(other.include_mcp_json); - // self.template = self.template.or(other.template); - // self.description = self.description.or(other.description); - // self.prompt = self.prompt.or(other.prompt); - // self.model = self.model.or(other.model); - - // // Collections are extended (merged) - // self.resources.extend(other.resources); - // self.tools.tools.extend(other.tools.tools); - // self.allowed_tools - // .allowed - // .extend(other.allowed_tools.allowed); - // self.tool_aliases.extend(other.tool_aliases); - // self.mcp.extend(other.mcp); - // self.inherits.parents.extend(other.inherits.parents); - // self.tool_settings.extend(other.tool_settings); - - // // Hooks are deep merged - // self.hook = match (self.hook, other.hook) { - // (None, Some(h)) => Some(h), - // (Some(a), Some(b)) => Some(a.merge(b)), - // (Some(a), None) => Some(a), - // (None, None) => None, - // }; - - // // Native tools are deep merged - // self.native_tool = self.native_tool.merge(other.native_tool); - - // self + // Child wins for explicit values + self.include_mcp_json = self.include_mcp_json.or(other.include_mcp_json); + self.template = self.template.or(other.template); + self.description = self.description.or(other.description); + self.prompt = self.prompt.or(other.prompt); + self.model = self.model.or(other.model); + + // Collections are extended (merged) + self.resources.extend(other.resources); + self.tools.extend(other.tools); + self.allowed_tools.extend(other.allowed_tools); + self.alias.extend(other.alias); + self.mcp.extend(other.mcp); + self.inherits.extend(other.inherits); + self.tool_setting.extend(other.tool_setting); + + self.hook = self.hook.merge(other.hook); + self.native_tool = self.native_tool.merge(other.native_tool); + + self } } diff --git a/src/config/native.rs b/src/config/native.rs index 75a51b4..12306c8 100644 --- a/src/config/native.rs +++ b/src/config/native.rs @@ -1,11 +1,13 @@ use { crate::agent::{ - AwsTool as KiroAwsTool, ExecuteShellTool as KiroShellTool, ReadTool as KiroReadTool, + AwsTool as KiroAwsTool, + ExecuteShellTool as KiroShellTool, + ReadTool as KiroReadTool, WriteTool as KiroWriteTool, }, facet::Facet, facet_kdl as kdl, - std::{collections::HashSet, fmt::Display}, + std::collections::HashSet, }; #[derive(Facet, Debug, PartialEq, Clone, Eq, Hash)] @@ -115,18 +117,23 @@ pub struct NativeTools { pub write: WriteTool, } +impl From for NativeTools { + fn from(value: NativeToolsDoc) -> Self { + Self { + shell: value.shell.into(), + aws: value.aws.into(), + read: value.read.into(), + write: value.write.into(), + } + } +} + #[derive(Facet, Debug, Clone, Default, PartialEq, Eq)] pub struct GenericList { #[facet(kdl::arguments)] pub list: Vec, } -impl GenericList { - pub fn into_set(self) -> HashSet { - HashSet::from_iter(self.list) - } -} - impl From<&'static str> for GenericList { fn from(value: &'static str) -> Self { Self { diff --git a/src/generator/discover.rs b/src/generator/discover.rs index 5f1b149..bf15f7b 100644 --- a/src/generator/discover.rs +++ b/src/generator/discover.rs @@ -1,6 +1,7 @@ use { super::*, - crate::config::{GeneratorConfig, KdlAgent}, + crate::config::{GeneratorConfig, GeneratorConfigDoc, KdlAgent, KdlAgentDoc}, + miette::IntoDiagnostic, std::{fmt::Display, ops::Deref, path::Path}, }; @@ -10,14 +11,10 @@ pub fn load_inline(fs: &Fs, path: impl AsRef) -> Result { .read_to_string_sync(&path) .wrap_err_with(|| format!("failed to read path '{}'", path.as_ref().display()))?; - match facet_kdl::from_str(&content) { - Ok(c) => Ok(c), - Err(e) => { - let err_msg = e.to_string(); - // eprintln!("{:?}", miette::Report::new(e)); - Err(eyre!("failed to parse: {err_msg}")) - } - } + let doc: GeneratorConfigDoc = facet_kdl::from_str(&content) + .into_diagnostic() + .map_err(|e| eyre!("failed to parse from {} err:{e}", path.as_ref().display()))?; + Ok(doc.into()) } else { Ok(GeneratorConfig::default()) } @@ -31,16 +28,17 @@ fn process_local( sources: &mut Vec, ) -> Result { let local_agent_path = location.local(&name); - let result = KdlAgent::from_path(fs, &name, &local_agent_path)?; + let result = KdlAgentDoc::from_path(fs, &name, &local_agent_path)?; match result { - None => Ok(KdlAgent::new(name)), + None => Ok(KdlAgent::new(name.as_ref().to_string())), Some(a) => { sources.push(KdlAgentSource::LocalFile(local_agent_path)); + let agent = KdlAgent::from(a.clone()); if let Some(i) = inline { sources.push(KdlAgentSource::LocalInline); - Ok(a.merge(i.clone())) + Ok(agent.merge(i.clone())) } else { - Ok(a) + Ok(agent) } } } @@ -98,8 +96,10 @@ pub fn discover( let local_agents: GeneratorConfig = load_inline(fs, local_path)?; tracing::debug!("found {} local agents", local_agents.agent.len()); - let local_names = local_agents.names(); - let global_names = global_agents.names(); + let local_names: HashSet = + HashSet::from_iter(local_agents.agent.keys().map(|k| k.to_string())); + let global_names: HashSet = + HashSet::from_iter(global_agents.agent.keys().map(|k| k.to_string())); let mut all_agents_names: HashSet = HashSet::with_capacity(global_names.len() + local_names.len()); all_agents_names.extend(local_names.clone()); @@ -128,19 +128,20 @@ pub fn discover( agent_sources.push(KdlAgentSource::GlobalInline); result = result.merge(a.clone()); } - let maybe_global_file = KdlAgent::from_path(fs, name, location.global(name))?; + let maybe_global_file = KdlAgentDoc::from_path(fs, name, location.global(name))?; if let Some(global) = maybe_global_file { agent_sources.push(KdlAgentSource::GlobalFile(location.global(name))); - result = result.merge(global.clone()); + result = result.merge(KdlAgent::from(global.clone())); } resolved_agents.insert(name.to_string(), result); } ConfigLocation::Global(_) => { - let mut global_file = match KdlAgent::from_path(fs, name, location.global(name))? { - None => KdlAgent::new(name), + let mut global_file = match KdlAgentDoc::from_path(fs, name, location.global(name))? + { + None => KdlAgent::new(name.to_string()), Some(a) => { agent_sources.push(KdlAgentSource::GlobalFile(location.global(name))); - a + KdlAgent::from(a) } }; if let Some(inline) = global_agents.get(name) { diff --git a/src/generator/merge.rs b/src/generator/merge.rs index 7d3461e..5c6aa7a 100644 --- a/src/generator/merge.rs +++ b/src/generator/merge.rs @@ -18,7 +18,7 @@ impl Generator { visited.insert(agent.name.clone()); let mut chain = Vec::new(); - for parent_name in agent.inherits().iter() { + for parent_name in agent.inherits.iter() { let parent = self .resolved .agents diff --git a/src/generator/mod.rs b/src/generator/mod.rs index 02c55b5..7378067 100644 --- a/src/generator/mod.rs +++ b/src/generator/mod.rs @@ -36,34 +36,34 @@ impl AgentResult { match target { ToolTarget::Read => self .agent - .get_tool_read() - .override_paths() + .native_tool + .read + .overrides .iter() - .cloned() .map(|f| f.to_string()) .collect(), ToolTarget::Write => self .agent - .get_tool_write().overrides -// .override_paths() + .native_tool + .write + .overrides .iter() - .cloned() .map(|f| f.to_string()) .collect(), ToolTarget::Shell => self .agent - .get_tool_shell() - .override_commands() + .native_tool + .shell + .overrides .iter() - .cloned() .map(|f| f.to_string()) .collect(), _ => vec![], } } - pub fn resources(&self) -> Vec { - self.agent.resources().map(|s| s.to_string()).collect() + pub fn resources(&self) -> HashSet { + self.agent.resources.clone() } } diff --git a/src/output.rs b/src/output.rs index 2b8f8a7..c4afbc7 100644 --- a/src/output.rs +++ b/src/output.rs @@ -129,7 +129,7 @@ impl OutputFormat { // MCP servers (only enabled ones) let mut servers = Vec::new(); - for (k, v) in &result.agent.mcp_servers() { + for (k, v) in &result.agent.mcp { if !v.disabled { servers.push(k.clone()); } @@ -140,14 +140,14 @@ impl OutputFormat { // Allowed tools let mut allowed_tools: Vec = result .agent - .allowed_tools() + .allowed_tools .iter() .filter(|t| !t.is_empty()) .cloned() .collect(); allowed_tools.sort(); let mut enabled_tools = Vec::with_capacity(allowed_tools.len()); - let mcps = result.agent.mcp_servers(); + let mcps = &result.agent.mcp; for t in allowed_tools { if t.len() < 2 { continue; @@ -179,7 +179,7 @@ impl OutputFormat { } // resources - if let Some(resources) = serialize_yaml("", &result.resources()) { + if let Some(resources) = serialize_yaml("", &Vec::from_iter(result.resources())) { row.add_cell(resources); } From 0d36521d1649e4a3235aa97438a2edef2625cbe5 Mon Sep 17 00:00:00 2001 From: dougeEfresh Date: Thu, 1 Jan 2026 21:46:44 +0000 Subject: [PATCH 3/8] removed knuffle --- src/agent/hook.rs | 6 +- src/config.rs | 321 ++++++++++++++++++- src/config/agent.rs | 143 ++++----- src/config/agent_file.rs | 47 ++- src/config/hook.rs | 256 +++++++-------- src/config/mcp.rs | 59 ++-- src/config/merge.rs | 334 ++++++++++--------- src/config/native.rs | 659 +++++++++++++++++++------------------- src/generator/discover.rs | 6 +- src/kdl/agent.rs | 244 -------------- src/kdl/agent_file.rs | 78 ----- src/kdl/hook.rs | 348 -------------------- src/kdl/mcp.rs | 96 ------ src/kdl/merge.rs | 207 ------------ src/kdl/mod.rs | 292 ----------------- 15 files changed, 1062 insertions(+), 2034 deletions(-) delete mode 100644 src/kdl/agent.rs delete mode 100644 src/kdl/agent_file.rs delete mode 100644 src/kdl/hook.rs delete mode 100644 src/kdl/mcp.rs delete mode 100644 src/kdl/merge.rs delete mode 100644 src/kdl/mod.rs diff --git a/src/agent/hook.rs b/src/agent/hook.rs index 4c12ab3..e9d6527 100644 --- a/src/agent/hook.rs +++ b/src/agent/hook.rs @@ -4,7 +4,7 @@ use { }; const DEFAULT_TIMEOUT_MS: u64 = 30_000; -const DEFAULT_MAX_OUTPUT_SIZE: usize = 1024 * 10; +const DEFAULT_MAX_OUTPUT_SIZE: u64 = 1024 * 10; const DEFAULT_CACHE_TTL_SECONDS: u64 = 0; #[derive( @@ -47,7 +47,7 @@ pub struct Hook { /// Max output size of the hook before it is truncated #[serde(default = "Hook::default_max_output_size")] - pub max_output_size: usize, + pub max_output_size: u64, /// How long the hook output is cached before it will be executed again #[serde(default = "Hook::default_cache_ttl_seconds")] @@ -95,7 +95,7 @@ impl Hook { DEFAULT_TIMEOUT_MS } - fn default_max_output_size() -> usize { + fn default_max_output_size() -> u64 { DEFAULT_MAX_OUTPUT_SIZE } diff --git a/src/config.rs b/src/config.rs index 89e8f9c..237bbb6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,39 +5,342 @@ mod mcp; mod merge; mod native; -use std::{collections::HashMap, fmt::Debug}; - pub use agent::{KdlAgent, KdlAgentDoc}; +use { + crate::Fs, + facet::Facet, + facet_kdl as kdl, + miette::IntoDiagnostic, + std::{ + collections::{HashMap, HashSet}, + fmt::{Debug, Display}, + path::Path, + }, +}; + +pub(crate) type ConfigResult = miette::Result; +#[derive(Facet, Debug, Default, PartialEq, Clone, Eq, Hash)] +#[facet(default)] +pub(super) struct GenericItem { + #[facet(kdl::argument)] + pub item: String, +} + +impl Display for GenericItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.item) + } +} + +impl AsRef for GenericItem { + fn as_ref(&self) -> &str { + self.item.as_ref() + } +} + +#[derive(Facet, Copy, Default, Clone, Debug, PartialEq, Eq)] +#[facet(default)] +pub(super) struct IntDoc { + #[facet(kdl::argument)] + pub value: u64, +} +impl AsRef for IntDoc { + fn as_ref(&self) -> &u64 { + &self.value + } +} + +pub(super) fn split_newline(list: Vec) -> HashSet { + list.iter() + .flat_map(|f| f.item.split('\n')) + .map(str::trim) + .filter(|s| !s.is_empty() && s.is_ascii()) + .map(String::from) + .collect() +} + +pub fn kdl_parse_path<'facet: 'shape, 'shape, T>( + fs: &Fs, + content: impl AsRef, +) -> ConfigResult +where + T: facet::Facet<'facet>, +{ + Ok(kdl_parse("")?) +} + +pub fn kdl_parse<'input, 'facet: 'shape, 'shape, T>(content: &'input str) -> ConfigResult +where + T: facet::Facet<'facet>, + 'input: 'facet, +{ + kdl::from_str(content).into_diagnostic() +} #[derive(facet::Facet, Default)] pub struct GeneratorConfigDoc { #[facet(facet_kdl::children, default)] - pub agent: Vec, + pub agents: Vec, } #[derive(Default)] pub struct GeneratorConfig { - pub agent: HashMap, + pub agents: HashMap, } impl From for GeneratorConfig { fn from(value: GeneratorConfigDoc) -> Self { - let mut agent: HashMap = HashMap::with_capacity(value.agent.len()); - for a in value.agent { + let mut agent: HashMap = HashMap::with_capacity(value.agents.len()); + for a in value.agents { agent.insert(a.name.clone(), a.into()); } - Self { agent } + Self { agents: agent } } } impl Debug for GeneratorConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "agents={}", self.agent.len()) + writeln!(f, "agents={}", self.agents.len()) } } impl GeneratorConfig { pub fn get(&self, name: impl AsRef) -> Option<&KdlAgent> { - self.agent.get(name.as_ref()) + self.agents.get(name.as_ref()) + } +} + +#[cfg(test)] +mod tests { + use {super::*, crate::config::agent_file::KdlAgentFileDoc}; + + #[test_log::test] + fn test_agent_decoding() -> ConfigResult<()> { + let kdl_agents = indoc::indoc! {r#" + agent "test" include-mcp-json=#true { + inherits "parent" + description "This is a test agent" + prompt "Generate a test prompt" + resource "file://resource.md" + resource "file://README.md" + tools "*" + allowed-tools "@awsdocs" + hook { + agent-spawn "spawn" { + command "echo i have spawned" + timeout-ms 1000 + max-output-size 9000 + cache-ttl-seconds 2 + } + user-prompt-submit "submit" { + command "echo user submitted" + } + pre-tool-use "pre" { + command "echo before tool" + matcher "git.*" + } + post-tool-use "post" { + command "echo after tool" + } + stop "stop" { + command "echo stopped" + } + } + + mcp "awsdocs" { + command "aws-docs" + args """ + --verbose + --config=/path + """ + env "RUST_LOG" "debug" + env "PATH" "/usr/bin" + header "Authorization" "Bearer token" + timeout 5000 + } + + alias "execute_bash" "shell" + + native-tool { + write { + allow "./src/*" + allow "./scripts/**" + deny "Cargo.lock" + override "/tmp" + override "/var/log" + } + shell deny-by-default=#true { + allow "git status .*" + deny "git push .*" + override "git pull .*" + } + } + } + "#}; + + let config: GeneratorConfigDoc = kdl_parse(kdl_agents)?; + assert_eq!(config.agents.len(), 1); + let config = GeneratorConfig::from(config); + let agent = config.agents.get("test"); + assert!(agent.is_some()); + let agent = agent.unwrap().clone(); + assert_eq!(agent.name, "test"); + assert!(agent.model.is_none()); + assert!(!agent.is_template()); + let inherits = &agent.inherits; + assert_eq!(inherits.len(), 1); + assert_eq!(inherits.iter().next().unwrap(), "parent"); + assert!(agent.description.is_some()); + assert!(agent.prompt.is_some()); + assert!(agent.include_mcp_json.unwrap_or_default()); + let tools = &agent.tools; + assert_eq!(tools.len(), 1); + assert_eq!(tools.iter().next().unwrap(), "*"); + let resources = &agent.resources; + assert_eq!(resources.len(), 2); + assert!(resources.contains(&"file://resource.md".to_string())); + assert!(resources.contains(&"file://README.md".to_string())); + + let hooks = &agent.hook; + let hook = &hooks.agent_spawn.get("spawn"); + assert!(hook.is_some()); + let hook = hook.unwrap(); + assert_eq!(hook.command, "echo i have spawned"); + + // assert!(hooks.contains_key(&HookTrigger::PreToolUse)); + // assert!(hooks.contains_key(&HookTrigger::PostToolUse)); + // assert!(hooks.contains_key(&HookTrigger::Stop)); + // assert!(hooks.contains_key(&HookTrigger::UserPromptSubmit)); + + let allowed = &agent.allowed_tools; + assert_eq!(allowed.len(), 1); + assert_eq!(allowed.iter().next().unwrap(), "@awsdocs"); + + let mcp = &agent.mcp; + assert_eq!(mcp.len(), 1); + assert!(mcp.contains_key("awsdocs")); + let aws_docs = mcp.get("awsdocs").unwrap(); + assert_eq!(aws_docs.command, "aws-docs"); + assert_eq!(aws_docs.args, vec!["--verbose\n--config=/path"]); + assert!(!aws_docs.disabled); + assert_eq!(aws_docs.headers.len(), 1); + assert_eq!(aws_docs.env.len(), 2); + assert_eq!(aws_docs.timeout, 5000); + assert_eq!(agent.alias.len(), 1); + + Ok(()) + } + + #[test_log::test] + fn test_agent_empty() -> ConfigResult<()> { + let kdl_agents = r#" + agent "test" template=#true { + } + "#; + + let config: GeneratorConfigDoc = kdl_parse(kdl_agents)?; + let config = GeneratorConfig::from(config); + assert!(!format!("{config:?}").is_empty()); + assert_eq!(config.agents.len(), 1); + let agent = config.agents.get("test").unwrap(); + assert_eq!(agent.name, "test"); + assert!(agent.model.is_none()); + assert!(agent.is_template()); + + Ok(()) + } + + #[test_log::test] + fn test_agent_file_source() -> ConfigResult<()> { + let kdl_agent_file_source = r#" + description "agent from file" + prompt "Generate a test prompt" + resource "file://resource.md" + resource "file://README.md" + include-mcp-json #true + tools "*" + + allowed-tools "@awsdocs" + hook { + agent-spawn "spawn" { + command "echo i have spawned" + timeout-ms 1000 + max-output-size 9000 + cache-ttl-seconds 2 + } + user-prompt-submit "submit" { + command "echo user submitted" + } + pre-tool-use "pre" { + command "echo before tool" + matcher "git.*" + } + post-tool-use "post" { + command "echo after tool" + } + stop "stop" { + command "echo stopped" + } + } + + mcp "awsdocs" { + command "aws-docs" + args """ + --verbose + --config=/path + """ + env "RUST_LOG" "debug" + env "PATH" "/usr/bin" + header "Authorization" "Bearer token" + timeout 5000 + } + + alias "execute_bash" "shell" + + native-tool { + write { + allow "./src/*" + allow "./scripts/**" + deny "Cargo.lock" + override "/tmp" + override "/var/log" + } + shell deny-by-default=#true { + allow "git status .*" + deny "git push .*" + override "git pull .*" + } + } + "#; + + let agent: KdlAgentFileDoc = kdl_parse(kdl_agent_file_source)?; + assert_eq!( + agent.description.unwrap_or_default().to_string(), + "agent from file" + ); + + Ok(()) + } + + #[test_log::test] + fn test_tool_setting_invalid_json() -> ConfigResult<()> { + let _kdl = r#" + agent "test" { + tool-setting "bad" { + json "{ invalid json }" + } + } + "#; + // TODO + // let config: GeneratorConfig = kdl_parse(kdl)?; + // let result = config.agents[0].extra_tool_settings(); + // assert!(result.is_err()); + // assert!( + // result + // .unwrap_err() + // .to_string() + // .contains("Failed to parse JSON") + // ); + Ok(()) } } diff --git a/src/config/agent.rs b/src/config/agent.rs index 7a037b8..ae2de43 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -1,10 +1,14 @@ use { super::{ + GenericItem, hook::{HookDoc, HookPart}, mcp::CustomToolConfigDoc, - native::{NativeTools, NativeToolsDoc}, + native::{AwsTool, ExecuteShellTool, NativeTools, NativeToolsDoc, ReadTool, WriteTool}, + }, + crate::{ + agent::{CustomToolConfig, OriginalToolName}, + config::split_newline, }, - crate::agent::{CustomToolConfig, OriginalToolName}, color_eyre::eyre::WrapErr, facet::Facet, facet_kdl as kdl, @@ -15,45 +19,9 @@ use { }, }; -#[derive(Facet, Clone, Default, Debug)] -pub(super) struct Inherits { - #[facet(kdl::arguments)] - pub parents: Vec, -} - -#[derive(Facet, Clone, Default, Debug)] -pub(super) struct Tools { - #[facet(kdl::arguments)] - pub tools: Vec, -} - -#[derive(Facet, Clone, Default, Debug)] -pub(super) struct AllowedTools { - #[facet(kdl::arguments)] - pub allowed: Vec, -} - -#[derive(Facet, Clone, Default, Debug)] -pub(super) struct Resource { - #[facet(kdl::argument)] - pub location: String, -} - -impl PartialEq for Resource { - fn eq(&self, other: &Self) -> bool { - self.location.eq(&other.location) - } -} - -impl Hash for Resource { - fn hash(&self, state: &mut H) { - self.location.hash(state); - } -} -impl Eq for Resource {} - #[derive(Facet, Clone, Default, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub(super) struct ToolAliasKdl { + #[facet(default)] #[facet(kdl::argument)] from: String, #[facet(kdl::argument)] @@ -112,58 +80,54 @@ pub struct KdlAgent { } #[derive(Facet, Clone, Default)] -#[facet(rename = "kabel-case", default)] +#[facet(rename_all = "kebab-case", default)] pub struct KdlAgentDoc { #[facet(kdl::argument)] pub name: String, - #[facet(kdl::property)] + + #[facet(kdl::property, default)] pub template: Option, + #[facet(kdl::child, default)] - pub description: Option, - #[facet(kdl::child, default)] - pub(super) inherits: Inherits, + pub(super) description: Option, + + #[facet(kdl::children, default)] + pub(super) inherits: Vec, + #[facet(kdl::child, default)] - pub prompt: Option, + pub(super) prompt: Option, + #[facet(kdl::children, default)] - pub(super) resources: Vec, + pub(super) resources: Vec, + #[facet(kdl::property, default)] pub include_mcp_json: Option, + + #[facet(kdl::children, rename = "tools", default)] + pub(super) tools: Vec, + + #[facet(kdl::children, rename = "allowed_tools", default)] + pub(super) allowed_tools: Vec, + #[facet(kdl::child, default)] - pub(super) tools: Option, - #[facet(kdl::child, default)] - pub(super) allowed_tools: Option, - #[facet(kdl::child, default)] - pub model: Option, + pub(super) model: Option, + #[facet(kdl::child, default)] pub(super) hook: Option, + #[facet(kdl::children, default)] pub(super) mcp: Vec, + #[facet(kdl::children, default)] pub(super) alias: Vec, + #[facet(kdl::child, default)] pub native_tool: NativeToolsDoc, + #[facet(kdl::children, default)] pub(super) tool_setting: Vec, } -#[derive(Facet, Clone, Default, Debug)] -pub struct Description { - #[facet(kdl::argument)] - value: String, -} - -#[derive(Facet, Clone, Default, Debug)] -pub struct Prompt { - #[facet(kdl::argument)] - value: String, -} - -#[derive(Facet, Clone, Debug)] -pub struct Model { - #[facet(kdl::argument)] - value: String, -} - impl Debug for KdlAgent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name) @@ -180,8 +144,8 @@ impl From for KdlAgent { fn from(value: KdlAgentDoc) -> Self { Self { name: value.name.clone(), - description: value.description.as_ref().map(|f| f.value.clone()), - prompt: value.prompt.as_ref().map(|f| f.value.clone()), + description: value.description.as_ref().map(|f| f.item.clone()), + prompt: value.prompt.as_ref().map(|f| f.item.clone()), alias: value.tool_aliases(), allowed_tools: value.allowed_tools(), inherits: value.inherits(), @@ -189,7 +153,7 @@ impl From for KdlAgent { include_mcp_json: value.include_mcp_json, hook: value.hooks(), resources: value.resources(), - model: value.model.as_ref().map(|f| f.value.clone()), + model: value.model.as_ref().map(|f| f.item.clone()), mcp: value.mcp_servers(), tools: value.tools(), tool_setting: Default::default(), // TODO use facet::Value @@ -209,14 +173,31 @@ impl KdlAgent { pub fn is_template(&self) -> bool { self.template.is_some_and(|f| f) } + + pub fn get_tool_aws(&self) -> &AwsTool { + &self.native_tool.aws + } + + pub fn get_tool_read(&self) -> &ReadTool { + &self.native_tool.read + } + + pub fn get_tool_write(&self) -> &WriteTool { + &self.native_tool.write + } + + pub fn get_tool_shell(&self) -> &ExecuteShellTool { + &self.native_tool.shell + } } + impl KdlAgentDoc { pub fn prompt(&self) -> String { - self.prompt.clone().unwrap_or_default().value + self.prompt.clone().unwrap_or_default().item } pub fn description(&self) -> String { - self.description.clone().unwrap_or_default().value + self.description.clone().unwrap_or_default().item } pub fn new(name: impl AsRef) -> Self { @@ -242,25 +223,19 @@ impl KdlAgentDoc { } pub fn allowed_tools(&self) -> HashSet { - self.allowed_tools - .as_ref() - .map(|a| HashSet::from_iter(a.allowed.clone())) - .unwrap_or_default() + split_newline(self.allowed_tools.clone()) } pub fn tools(&self) -> HashSet { - self.tools - .as_ref() - .map(|t| HashSet::from_iter(t.tools.clone())) - .unwrap_or_default() + split_newline(self.tools.clone()) } pub fn inherits(&self) -> HashSet { - HashSet::from_iter(self.inherits.parents.clone()) + split_newline(self.inherits.clone()) } pub fn resources(&self) -> HashSet { - HashSet::from_iter(self.resources.iter().map(|r| r.location.clone())) + split_newline(self.resources.clone()) } pub fn mcp_servers(&self) -> HashMap { diff --git a/src/config/agent_file.rs b/src/config/agent_file.rs index 3a88375..41a27da 100644 --- a/src/config/agent_file.rs +++ b/src/config/agent_file.rs @@ -1,5 +1,11 @@ use { - super::{agent::*, hook::HookDoc, mcp::CustomToolConfigDoc, native::NativeToolsDoc}, + super::{ + GenericItem, + agent::*, + hook::HookDoc, + mcp::CustomToolConfigDoc, + native::NativeToolsDoc, + }, crate::Fs, color_eyre::eyre::eyre, facet::Facet, @@ -8,25 +14,36 @@ use { std::path::Path, }; +#[derive(Facet, Copy, Default, Clone, Debug, PartialEq, Eq)] +#[facet(default)] +pub(super) struct BoolDoc { + #[facet(kdl::argument)] + pub value: bool, +} #[derive(Facet, Clone, Default)] -#[facet(rename = "kabel-case", default)] +#[facet(rename_all = "kebab-case", default)] pub struct KdlAgentFileDoc { #[facet(kdl::child, default)] - pub description: Option, - #[facet(kdl::child, default)] - pub(super) inherits: Inherits, - #[facet(kdl::child, default)] - pub(super) prompt: Option, + pub(super) description: Option, #[facet(kdl::children, default)] - pub(super) resources: Vec, - #[facet(kdl::property, default)] - pub include_mcp_json: Option, + pub(super) inherits: Vec, #[facet(kdl::child, default)] - pub(super) tools: Option, + pub(super) prompt: Option, + #[facet(kdl::children, default)] + pub(super) resources: Vec, + #[facet(kdl::child, default)] - pub(super) allowed_tools: Option, + pub(super) include_mcp_json: Option, + + #[facet(kdl::children, default)] + pub(super) tools: Vec, + + #[facet(kdl::children, default)] + pub(super) allowed_tools: Vec, + #[facet(kdl::child, default)] - pub(super) model: Option, + pub(super) model: Option, + #[facet(kdl::child, default)] pub(super) hook: Option, #[facet(kdl::children, default)] @@ -61,10 +78,10 @@ impl KdlAgentDoc { name: name.as_ref().to_string(), description: file_source.description, template: None, - inherits: Inherits::default(), + inherits: file_source.inherits, prompt: file_source.prompt, resources: file_source.resources, - include_mcp_json: file_source.include_mcp_json, + include_mcp_json: Some(file_source.include_mcp_json.unwrap_or_default().value), tools: file_source.tools, allowed_tools: file_source.allowed_tools, model: file_source.model, diff --git a/src/config/hook.rs b/src/config/hook.rs index dfe8777..00991e3 100644 --- a/src/config/hook.rs +++ b/src/config/hook.rs @@ -1,40 +1,11 @@ use { + super::IntDoc, crate::agent::hook::{Hook, HookTrigger}, facet::Facet, facet_kdl as kdl, std::collections::HashMap, }; -#[derive(Facet, Clone, Debug, PartialEq, Eq)] -struct Command { - #[facet(kdl::argument)] - value: String, -} - -#[derive(Facet, Clone, Debug, PartialEq, Eq)] -struct TimeoutMs { - #[facet(kdl::argument)] - value: usize, -} - -#[derive(Facet, Clone, Debug, PartialEq, Eq)] -struct MaxOutputSize { - #[facet(kdl::argument)] - value: usize, -} - -#[derive(Facet, Clone, Debug, PartialEq, Eq)] -struct CacheTtlSeconds { - #[facet(kdl::argument)] - value: usize, -} - -#[derive(Facet, Clone, Debug, PartialEq, Eq)] -struct Matcher { - #[facet(kdl::argument)] - value: String, -} - macro_rules! define_hook_doc { ($name:ident) => { #[derive(Facet, Default, Clone, Debug, PartialEq, Eq)] @@ -57,9 +28,9 @@ macro_rules! define_hook_doc { fn from(value: $name) -> Hook { Hook { command: value.command.value, - timeout_ms: value.timeout_ms.value as u64, + timeout_ms: value.timeout_ms.value, max_output_size: value.max_output_size.value, - cache_ttl_seconds: value.cache_ttl_seconds.value as u64, + cache_ttl_seconds: value.cache_ttl_seconds.value, matcher: value.matcher.map(|m| m.value), } } @@ -74,13 +45,6 @@ struct GenericValue { value: String, } -#[derive(Facet, Default, Clone, Debug, PartialEq, Eq)] -#[facet(default)] -struct IntDoc { - #[facet(kdl::argument)] - value: usize, -} - define_hook_doc!(HookAgentSpawnDoc); define_hook_doc!(HookUserPromptSubmitDoc); define_hook_doc!(HookPreToolUseDoc); @@ -183,98 +147,138 @@ fn merge_hooks( base } -// #[cfg(test)] -// mod tests { -// use {super::*, crate::Result, std::time::Duration}; +#[cfg(test)] +mod tests { + use {super::*, crate::Result, std::time::Duration}; -// macro_rules! rando_hook { -// ($name:ident) => { -// impl $name { -// fn rando() -> $name { -// let value = std::time::SystemTime::now() -// .duration_since(std::time::UNIX_EPOCH) -// .unwrap() -// .as_secs(); -// Self { -// name: format!("$name-{value}"), -// command: Some(Command { -// value: format!("{value}"), -// }), -// timeout_ms: Some(TimeoutMs { value }), -// max_output_size: None, -// cache_ttl_seconds: Some(CacheTtlSeconds { value }), -// matcher: Some(Matcher { -// value: format!("{value}"), -// }), -// } -// } -// } -// }; -// } -// rando_hook!(HookAgentSpawn); -// rando_hook!(HookUserPromptSubmit); -// rando_hook!(HookPreToolUse); -// rando_hook!(HookPostToolUse); -// rando_hook!(HookStop); + fn rando() -> HashMap { + let value = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); -// impl HookPart { -// pub fn randomize() -> Self { -// Self { -// agent_spawn: vec![HookAgentSpawn::rando()], -// user_prompt_submit: vec![HookUserPromptSubmit::rando()], -// pre_tool_use: vec![HookPreToolUse::rando()], -// post_tool_use: vec![HookPostToolUse::rando()], -// stop: vec![HookStop::rando()], -// } -// } -// } + let name = format!("$name-{value}"); + let mut hooks = HashMap::new(); + hooks.insert(name, Hook { + command: format!("{value}"), + timeout_ms: value, + max_output_size: value, + cache_ttl_seconds: value, + matcher: Some(format!("{value}")), + }); + hooks + } -// #[test_log::test] -// pub fn test_hooks_empty() -> Result<()> { -// let child = HookPart::default(); -// let parent = HookPart::default(); -// let merged = child.merge(parent); + impl HookPart { + pub fn randomize() -> Self { + Self { + agent_spawn: rando(), + user_prompt_submit: rando(), + pre_tool_use: rando(), + post_tool_use: rando(), + stop: rando(), + } + } + } + #[test_log::test] + pub fn test_hooks_kdl() -> Result<()> { + let kdl = r#" + agent-spawn "test" { + command "echo" + timeout-ms 1231 + max-output-size 69 + cache-ttl-seconds 666 + matcher "blah" + } + user-prompt-submit "prompt-hook" { + command "validate-prompt" + timeout-ms 500 + } + pre-tool-use "pre-hook" { + command "pre-check" + matcher "git*" + } + post-tool-use "post-hook" { + command "post-check" + } + stop "cleanup" { + command "cleanup-script" + cache-ttl-seconds 0 + } + "#; + let doc: HookDoc = facet_kdl::from_str(kdl)?; + let doc = HookPart::from(doc); + + assert_eq!(1, doc.agent_spawn.len()); + let hook = doc.agent_spawn.get("test"); + assert!(hook.is_some()); + let hook = hook.unwrap(); + assert_eq!(hook.command, "echo"); + assert_eq!(hook.timeout_ms, 1231); + assert_eq!(hook.max_output_size, 69); -// assert!(merged.agent_spawn.is_empty()); -// assert!(merged.user_prompt_submit.is_empty()); -// assert!(merged.pre_tool_use.is_empty()); -// assert!(merged.post_tool_use.is_empty()); -// assert!(merged.stop.is_empty()); -// Ok(()) -// } + assert_eq!(1, doc.user_prompt_submit.len()); + assert!(doc.user_prompt_submit.contains_key("prompt-hook")); -// #[test_log::test] -// pub fn test_hooks_empty_child() -> Result<()> { -// let child = HookPart::default(); -// let parent = HookPart::randomize(); -// let before = parent.clone(); -// let merged = child.merge(parent); + assert_eq!(1, doc.pre_tool_use.len()); + let pre = doc.pre_tool_use.get("pre-hook").unwrap(); + assert_eq!(pre.matcher, Some("git*".to_string())); -// assert_eq!(merged, before); -// Ok(()) -// } + assert_eq!(1, doc.post_tool_use.len()); + assert!(doc.post_tool_use.contains_key("post-hook")); -// #[test_log::test] -// pub fn test_hooks_no_merge() -> Result<()> { -// let child = HookPart::randomize(); -// let parent = HookPart::randomize(); -// let before = child.clone(); -// let merged = child.merge(parent); -// assert_eq!(merged, before); -// Ok(()) -// } + assert_eq!(1, doc.stop.len()); + assert!(doc.stop.contains_key("cleanup")); + + Ok(()) + } -// #[test_log::test] -// pub fn test_hooks_merge_parent() -> Result<()> { -// let child = HookPart::randomize(); -// std::thread::sleep(Duration::from_millis(1300)); -// let parent = HookPart::randomize(); -// let merged = child.merge(parent); -// assert_eq!(merged.agent_spawn.len(), 2); -// assert_eq!(merged.user_prompt_submit.len(), 2); -// assert_eq!(merged.pre_tool_use.len(), 2); -// assert_eq!(merged.post_tool_use.len(), 2); -// assert_eq!(merged.stop.len(), 2); -// Ok(()) -// } -// } + #[test_log::test] + pub fn test_hooks_empty() -> Result<()> { + let child = HookPart::default(); + let parent = HookPart::default(); + let merged = child.merge(parent); + + assert!(merged.agent_spawn.is_empty()); + assert!(merged.user_prompt_submit.is_empty()); + assert!(merged.pre_tool_use.is_empty()); + assert!(merged.post_tool_use.is_empty()); + assert!(merged.stop.is_empty()); + Ok(()) + } + + #[test_log::test] + pub fn test_hooks_empty_child() -> Result<()> { + let child = HookPart::default(); + let parent = HookPart::randomize(); + let before = parent.clone(); + let merged = child.merge(parent); + + assert_eq!(merged, before); + Ok(()) + } + + #[test_log::test] + pub fn test_hooks_no_merge() -> Result<()> { + let child = HookPart::randomize(); + let parent = HookPart::randomize(); + let before = child.clone(); + let merged = child.merge(parent); + assert_eq!(merged, before); + Ok(()) + } + + #[test_log::test] + pub fn test_hooks_merge_parent() -> Result<()> { + let child = HookPart::randomize(); + std::thread::sleep(Duration::from_millis(1300)); + let parent = HookPart::randomize(); + let merged = child.merge(parent); + assert_eq!(merged.agent_spawn.len(), 2); + assert_eq!(merged.user_prompt_submit.len(), 2); + assert_eq!(merged.pre_tool_use.len(), 2); + assert_eq!(merged.post_tool_use.len(), 2); + assert_eq!(merged.stop.len(), 2); + Ok(()) + } +} diff --git a/src/config/mcp.rs b/src/config/mcp.rs index 669ba44..c79a41b 100644 --- a/src/config/mcp.rs +++ b/src/config/mcp.rs @@ -1,4 +1,9 @@ -use {crate::agent::CustomToolConfig, facet::Facet, facet_kdl as kdl}; +use { + super::IntDoc, + crate::{agent::CustomToolConfig, config::GenericItem}, + facet::Facet, + facet_kdl as kdl, +}; #[derive(Facet, Clone, Debug)] struct EnvVar { @@ -16,12 +21,6 @@ struct Header { value: String, } -#[derive(Facet, Default, Clone, Debug)] -struct ToolArgs { - #[facet(kdl::arguments, default)] - args: Vec, -} - #[derive(Facet, Clone, Debug, Eq, PartialEq)] pub struct RedirectUri { #[facet(kdl::argument)] @@ -29,6 +28,13 @@ pub struct RedirectUri { } #[derive(Facet, Clone, Debug)] +pub struct Disabled { + #[facet(kdl::argument)] + pub value: bool, +} + +#[derive(Facet, Default, Clone, Debug)] +#[facet(rename_all = "kebab-case", default)] pub struct CustomToolConfigDoc { #[facet(kdl::argument)] pub name: String, @@ -39,8 +45,8 @@ pub struct CustomToolConfigDoc { #[facet(kdl::child, default)] pub command: Option, - #[facet(kdl::child, default)] - args: Option, + #[facet(kdl::children, default)] + args: Vec, #[facet(kdl::children, default)] env: Vec, @@ -49,7 +55,7 @@ pub struct CustomToolConfigDoc { header: Vec
, #[facet(kdl::child, default)] - pub timeout: Option, + pub(super) timeout: IntDoc, #[facet(kdl::child, default)] pub disabled: Option, @@ -67,18 +73,6 @@ pub struct Command { pub value: String, } -#[derive(Facet, Clone, Debug)] -pub struct Timeout { - #[facet(kdl::argument)] - pub value: u64, -} - -#[derive(Facet, Clone, Debug)] -pub struct Disabled { - #[facet(kdl::argument)] - pub value: bool, -} - impl From for CustomToolConfig { fn from(value: CustomToolConfigDoc) -> Self { let command = value.command.map(|c| c.value).unwrap_or_default(); @@ -87,12 +81,12 @@ impl From for CustomToolConfig { Self { url, command, - args: value.args.map(|a| a.args).unwrap_or_default(), - timeout: value - .timeout - .map(|t| t.value) - .filter(|&t| t != 0) - .unwrap_or_else(crate::agent::tool_default_timeout), + args: value.args.into_iter().map(|v| v.item).collect(), + timeout: if value.timeout.value == 0 { + crate::agent::tool_default_timeout() + } else { + value.timeout.value + }, disabled: value.disabled.map(|d| d.value).unwrap_or(false), headers: value.header.into_iter().map(|h| (h.key, h.value)).collect(), env: value.env.into_iter().map(|e| (e.key, e.value)).collect(), @@ -126,7 +120,7 @@ mod tests { let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); assert_eq!(doc.mcp.name, "rustdocs"); assert_eq!(doc.mcp.command.unwrap().value, "rust-docs-mcp"); - assert_eq!(doc.mcp.timeout.unwrap().value, 1000); + assert_eq!(doc.mcp.timeout.value, 1000); } #[test] @@ -163,11 +157,8 @@ mod tests { }"#; let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); - assert_eq!(doc.mcp.args.unwrap().args, vec![ - "--verbose", - "--output", - "json" - ]); + let args: Vec = doc.mcp.args.into_iter().map(|v| v.item).collect(); + assert_eq!(args, vec!["--verbose", "--output", "json"]); } #[test] diff --git a/src/config/merge.rs b/src/config/merge.rs index 8068819..99b037b 100644 --- a/src/config/merge.rs +++ b/src/config/merge.rs @@ -25,173 +25,167 @@ impl KdlAgent { } } -// #[cfg(test)] -// mod tests { -// use {super::*, crate::agent::hook::HookTrigger, color_eyre::eyre::eyre, -// knuffel::parse}; - -// #[test_log::test] -// fn test_agent_merge() -> crate::Result<()> { -// let kdl_agents = r#" -// agent "child" template=$false { -// description "I am a child" -// resource "file://child.md" -// resource "file://README.md" -// inherits "parent" -// include-mcp-json true -// tools "@awsdocs" "shell" -// native-tool { -// write { -// override "Cargo.lock" -// } -// shell { -// override "git push .*" -// } -// } -// hook { -// agent-spawn "spawn" { -// command "echo i have spawned" -// max-output-size 9000 -// cache-ttl-seconds 2 -// } -// } -// alias "execute_bash" "shell" -// } -// agent "parent" template=#true { -// description "I am parent" -// resource "file://parent.md" -// resource "file://README.md" -// tools "web_search" "shell" -// prompt "i tell you what to do" -// model "claude" -// allowed-tools "write" -// alias "execute_bash" "shell" -// alias "fs_read" "read" -// native-tool { -// read { -// allow "./src/*" "./scripts/**" -// deny "Cargo.lock" -// } -// write { -// allow "./src/*" "./scripts/**" -// deny "Cargo.lock" -// } - -// shell { -// allow "git status .*" "git pull .*" -// deny "git push .*" -// } -// } -// hook { -// agent-spawn "spawn" { -// timeout-ms 1111 -// } -// user-prompt-submit "submit" { -// command "echo user submitted" -// timeout-ms 1000 -// } -// pre-tool-use "pre" { -// command "echo before tool" -// matcher "git.*" -// } -// post-tool-use "post" { -// command "echo after tool" -// } -// stop "stop" { -// command "echo stopped" -// } -// } -// } -// "#; - -// let config: GeneratorConfig = match parse("example.kdl", kdl_agents) -// { Ok(c) => c, -// Err(e) => { -// eprintln!("{:?}", miette::Report::new(e)); -// return Err(eyre!("failed to parse {kdl_agents}")); -// } -// }; -// assert_eq!(config.agents.len(), 2); -// let child = config -// .agents -// .iter() -// .find(|a| a.name == "child") -// .unwrap() -// .clone(); -// let parent = config -// .agents -// .iter() -// .find(|a| a.name == "parent") -// .unwrap() -// .clone(); -// let merged = child.merge(parent); -// assert!(merged.description.is_some()); -// let d = merged.description.clone().unwrap(); -// assert_eq!(d, "I am a child"); - -// assert_eq!(merged.resources.len(), 3); -// assert!(!merged.is_template()); -// assert!(merged.include_mcp_json()); - -// assert_eq!(merged.inherits.parents.len(), 1); -// assert!(merged.inherits.parents.contains("parent")); - -// assert_eq!(merged.prompt, Some("i tell you what to do".to_string())); -// let tools = merged.tools(); -// assert_eq!(tools.len(), 3); -// assert!(tools.contains("@awsdocs")); -// assert!(tools.contains("shell")); -// assert!(tools.contains("web_search")); - -// assert_eq!(merged.model, Some("claude".to_string())); - -// let allowed_tools = merged.allowed_tools(); -// assert_eq!(allowed_tools.len(), 1); -// assert!(allowed_tools.contains("write")); - -// let hooks = merged.hooks(); -// assert!(!hooks.is_empty()); -// let h = hooks.get(&HookTrigger::AgentSpawn); -// assert!(h.is_some()); -// let h = h.unwrap(); -// assert!(!h.is_empty()); -// assert_eq!(h[0].timeout_ms, 1111); -// assert_eq!(h[0].command, "echo i have spawned"); - -// let h = hooks.get(&HookTrigger::UserPromptSubmit); -// assert!(h.is_some()); -// let h = h.unwrap(); -// assert!(!h.is_empty()); -// assert_eq!(h[0].command, "echo user submitted"); -// assert_eq!(h[0].timeout_ms, 1000); - -// let alias = merged.tool_aliases(); -// assert_eq!(alias.len(), 2); -// assert!(alias.contains_key("fs_read")); -// assert!(alias.contains_key("execute_bash")); - -// let tool = merged.get_tool_write(); -// assert!(tool.override_path.contains(&"Cargo.lock".into())); -// assert_eq!(tool.allow.list.len(), 2); -// assert_eq!(tool.override_path.len(), 1); -// assert_eq!(tool.deny.list.len(), 1); - -// let tool = merged.get_tool_read(); -// assert_eq!(tool.allow.list.len(), 2); -// assert_eq!(tool.override_path.len(), 0); -// assert_eq!(tool.deny.list.len(), 1); - -// let tool = merged.get_tool_shell(); -// assert_eq!(tool.allow.list.len(), 2); -// assert_eq!(tool.override_command.len(), 1); -// assert_eq!(tool.deny.list.len(), 1); - -// let tool = merged.get_tool_aws(); -// assert!(tool.allow.list.is_empty()); -// assert!(tool.deny.list.is_empty()); - -// assert_eq!("child", format!("{merged}")); -// assert_eq!("child", format!("{merged:?}")); -// Ok(()) -// } -// } +#[cfg(test)] +mod tests { + use { + super::*, + crate::{agent::hook::HookTrigger, config}, + }; + + #[test_log::test] + fn test_agent_merge() -> config::ConfigResult<()> { + let kdl_agents = indoc::indoc! {r#" + agent "child" template=#false include-mcp-json=#true { + description "I am a child" + resource "file://child.md" + resource "file://README.md" + inherit "parent" + tool "@awsdocs" + tool "shell" + native-tool { + write { + override "Cargo.lock" + } + shell { + override "git push .*" + } + } + hook { + agent-spawn "spawn" { + command "echo i have spawned" + max-output-size 9000 + cache-ttl-seconds 2 + } + } + alias "execute_bash" "shell" + } + agent "parent" template=#true { + description "I am parent" + resource "file://parent.md" + resource "file://README.md" + tool "web_search" + tool "shell" + prompt "i tell you what to do" + model "claude" + allowed-tool "write" + alias "execute_bash" "shell" + alias "fs_read" "read" + native-tool { + read { + allow "./src/*" + allow "./scripts/**" + deny "Cargo.lock" + } + write { + allow "./src/*" + allow "./scripts/**" + deny "Cargo.lock" + } + + shell { + allow "git status .*" + allow "git pull .*" + deny "git push .*" + } + } + hook { + agent-spawn "spawn" { + timeout-ms 1111 + } + user-prompt-submit "submit" { + command "echo user submitted" + timeout-ms 1000 + } + pre-tool-use "pre" { + command "echo before tool" + matcher "git.*" + } + post-tool-use "post" { + command "echo after tool" + } + stop "stop" { + command "echo stopped" + } + } + } + "#}; + + let config: GeneratorConfigDoc = config::kdl_parse(kdl_agents)?; + assert_eq!(config.agents.len(), 2); + let config = GeneratorConfig::from(config); + let child = config.agents.get("child"); + let parent = config.agents.get("parent"); + assert!(child.is_some()); + assert!(parent.is_some()); + let child = child.unwrap().clone(); + let parent = parent.unwrap().clone(); + assert_eq!("child", child.name); + assert_eq!("parent", parent.name); + assert!(!child.tools.is_empty()); + assert!(!parent.tools.is_empty()); + assert!(parent.is_template()); + let merged = child.merge(parent); + assert!(merged.description.is_some()); + let d = merged.description.clone().unwrap(); + assert_eq!(d, "I am a child"); + + assert_eq!(merged.resources.len(), 3); + assert!(!merged.is_template()); + assert!(merged.include_mcp_json.unwrap_or_default()); + + assert_eq!(merged.inherits.len(), 1); + assert!(merged.inherits.contains("parent")); + + assert_eq!(merged.prompt, Some("i tell you what to do".to_string())); + let tools = &merged.tools; + assert_eq!(tools.len(), 3); + assert!(tools.contains("@awsdocs")); + assert!(tools.contains("shell")); + assert!(tools.contains("web_search")); + + assert_eq!(merged.model, Some("claude".to_string())); + + let allowed_tools = &merged.allowed_tools; + assert_eq!(allowed_tools.len(), 1); + assert!(allowed_tools.contains("write")); + + let hooks = &merged.hook.hooks(&HookTrigger::AgentSpawn); + assert!(!hooks.is_empty()); + assert_eq!(hooks[0].timeout_ms, 1111); + assert_eq!(hooks[0].command, "echo i have spawned"); + + let hooks = &merged.hook.hooks(&HookTrigger::UserPromptSubmit); + assert!(!hooks.is_empty()); + assert_eq!(hooks[0].command, "echo user submitted"); + assert_eq!(hooks[0].timeout_ms, 1000); + + let alias = &merged.alias; + assert_eq!(alias.len(), 2); + assert!(alias.contains_key("fs_read")); + assert!(alias.contains_key("execute_bash")); + + let tool = merged.get_tool_write(); + assert!(tool.overrides.contains("Cargo.lock")); + assert_eq!(tool.allows.len(), 2); + assert_eq!(tool.overrides.len(), 1); + assert_eq!(tool.denies.len(), 1); + + let tool = merged.get_tool_read(); + assert_eq!(tool.allows.len(), 2); + assert_eq!(tool.overrides.len(), 0); + assert_eq!(tool.denies.len(), 1); + + let tool = merged.get_tool_shell(); + assert_eq!(tool.allows.len(), 2); + assert_eq!(tool.overrides.len(), 1); + assert_eq!(tool.denies.len(), 1); + + let tool = merged.get_tool_aws(); + assert!(tool.allows.is_empty()); + assert!(tool.denies.is_empty()); + + assert_eq!("child", format!("{merged}")); + assert_eq!("child", format!("{merged:?}")); + Ok(()) + } +} diff --git a/src/config/native.rs b/src/config/native.rs index 12306c8..5bdaf2e 100644 --- a/src/config/native.rs +++ b/src/config/native.rs @@ -1,4 +1,5 @@ use { + super::{GenericItem, split_newline}, crate::agent::{ AwsTool as KiroAwsTool, ExecuteShellTool as KiroShellTool, @@ -10,11 +11,6 @@ use { std::collections::HashSet, }; -#[derive(Facet, Debug, PartialEq, Clone, Eq, Hash)] -pub struct GenericListItem { - #[facet(kdl::argument)] - item: String, -} macro_rules! define_tool { ($name:ident) => { #[derive(Clone, Debug, Default, PartialEq, Eq)] @@ -30,6 +26,7 @@ macro_rules! define_tool { pub fn merge(mut self, other: Self) -> Self { self.allows.extend(other.allows); self.denies.extend(other.denies); + self.overrides.extend(other.overrides); self.disable_auto_readonly = self.disable_auto_readonly.or(other.disable_auto_readonly); self.deny_by_default = self.deny_by_default.or(other.deny_by_default); @@ -45,11 +42,11 @@ macro_rules! define_kdl_doc { #[facet(default, rename_all = "kebab-case")] pub struct $name { #[facet(default, kdl::children)] - pub allows: Vec, + pub(super) allows: Vec, #[facet(default, kdl::children)] - pub denies: Vec, + pub(super) denies: Vec, #[facet(default, kdl::children)] - pub overrides: Vec, + pub(super) overrides: Vec, #[facet(default, kdl::property)] pub deny_by_default: Option, #[facet(default, kdl::property)] @@ -87,17 +84,8 @@ define_tool_into!(AwsToolDoc, AwsTool); define_tool_into!(WriteToolDoc, WriteTool); define_tool_into!(ReadToolDoc, ReadTool); -fn split_newline(list: Vec) -> HashSet { - let values: Vec<&str> = list.iter().flat_map(|f| f.item.split("\n")).collect(); - let mut combined: Vec = vec![]; - for v in values { - combined.push(v.to_string()); - } - HashSet::from_iter(combined) -} - #[derive(Facet, Default, Clone, Debug, PartialEq, Eq)] -#[facet(deny_unknown_fields)] +#[facet(default, deny_unknown_fields)] pub struct NativeToolsDoc { #[facet(default, kdl::child)] pub shell: ExecuteShellToolDoc, @@ -166,7 +154,7 @@ impl From<&NativeTools> for KiroAwsTool { KiroAwsTool { allowed_services: aws.allows.clone(), denied_services: aws.denies.clone(), - auto_allow_readonly: aws.disable_auto_readonly.unwrap_or(true), + auto_allow_readonly: !aws.disable_auto_readonly.unwrap_or(false), } } } @@ -174,24 +162,24 @@ impl From<&NativeTools> for KiroAwsTool { impl From<&NativeTools> for KiroWriteTool { fn from(value: &NativeTools) -> Self { let write = &value.write; - let mut allow: HashSet = write.allows.clone(); - let mut deny: HashSet = write.denies.clone(); + let mut allows: HashSet = write.allows.clone(); + let mut denies: HashSet = write.denies.clone(); if !write.overrides.is_empty() { tracing::trace!( "Override/Forcing write: {:?}", write.overrides.iter().collect::>() ); for cmd in write.overrides.iter() { - allow.insert(cmd.clone()); - if deny.remove(cmd) { - tracing::trace!("Removed from deny: {cmd}"); + allows.insert(cmd.clone()); + if denies.remove(cmd) { + tracing::trace!("Removed from denies: {cmd}"); } } } Self { - allowed_paths: allow, - denied_paths: deny, + allowed_paths: allows, + denied_paths: denies, } } } @@ -199,24 +187,24 @@ impl From<&NativeTools> for KiroWriteTool { impl From<&NativeTools> for KiroReadTool { fn from(value: &NativeTools) -> Self { let read = &value.read; - let mut allow: HashSet = read.allows.clone(); - let mut deny: HashSet = read.denies.clone(); + let mut allows: HashSet = read.allows.clone(); + let mut denies: HashSet = read.denies.clone(); if !read.overrides.is_empty() { tracing::trace!( "Override/Forcing write: {:?}", read.overrides.iter().collect::>() ); for cmd in read.overrides.iter() { - allow.insert(cmd.clone()); - if deny.remove(cmd) { - tracing::trace!("Removed from deny: {cmd}"); + allows.insert(cmd.clone()); + if denies.remove(cmd) { + tracing::trace!("Removed from denies: {cmd}"); } } } Self { - allowed_paths: allow, - denied_paths: deny, + allowed_paths: allows, + denied_paths: denies, } } } @@ -224,8 +212,8 @@ impl From<&NativeTools> for KiroReadTool { impl From<&NativeTools> for KiroShellTool { fn from(value: &NativeTools) -> Self { let shell = &value.shell; - let mut allow: HashSet = shell.allows.clone(); - let mut deny: HashSet = shell.denies.clone(); + let mut allows: HashSet = shell.allows.clone(); + let mut denies: HashSet = shell.denies.clone(); if !shell.overrides.is_empty() { tracing::trace!( @@ -233,303 +221,324 @@ impl From<&NativeTools> for KiroShellTool { shell.overrides.iter().collect::>() ); for cmd in shell.overrides.iter() { - allow.insert(cmd.clone()); - if deny.remove(cmd) { - tracing::trace!("Removed command from deny: {cmd}"); + allows.insert(cmd.clone()); + if denies.remove(cmd) { + tracing::trace!("Removed command from denies: {cmd}"); } } } Self { - allowed_commands: allow, - denied_commands: deny, + allowed_commands: allows, + denied_commands: denies, deny_by_default: shell.deny_by_default.unwrap_or(false), auto_allow_readonly: shell.disable_auto_readonly.unwrap_or(true), } } } -// #[cfg(test)] -// mod tests { -// use {super::*, crate::Result}; - -// #[derive(Facet, Debug)] -// struct NativeToolsDoc { -// #[facet(kdl::child)] -// native: NativeTools, -// } - -// #[test_log::test] -// fn parse_shell_tool() { -// let kdl = r#"native { -// shell deny_by_default=#true disable_auto_readonly=#false { -// allow "ls .*" "git status" -// deny "rm -rf /" -// override "git push" -// } -// }"#; - -// let doc: NativeToolsDoc = facet_kdl::from_str(kdl).unwrap(); -// let shell = doc.native.shell.unwrap(); -// assert_eq!(shell.allow.unwrap().list.len(), 2); -// assert_eq!(shell.deny.unwrap().list.len(), 1); -// assert!(shell.deny_by_default.unwrap()); -// assert!(!shell.disable_auto_readonly.unwrap()); -// assert_eq!(shell.r#override.len(), 1); -// } - -// #[test_log::test] -// fn parse_aws_tool() { -// let kdl = r#"native { -// aws disable_auto_readonly=#true { -// allow "ec2" "s3" -// deny "iam" -// } -// }"#; - -// let doc: NativeToolsDoc = facet_kdl::from_str(kdl).unwrap(); -// let aws = doc.native.aws.unwrap(); -// assert!(aws.disable_auto_readonly.unwrap()); -// assert_eq!(aws.allow.unwrap().list.len(), 2); -// assert_eq!(aws.deny.unwrap().list.len(), 1); -// } - -// #[test_log::test] -// fn parse_read_write_tools() { -// let kdl = r#"native { -// read { -// allow "*.rs" "*.toml" -// deny "/etc/*" -// override "/etc/hosts" -// } -// write { -// allow "*.txt" -// deny "/tmp/*" -// override "/tmp/allowed" -// } -// }"#; - -// let doc: NativeToolsDoc = facet_kdl::from_str(kdl).unwrap(); -// assert_eq!(doc.native.read.unwrap().allow.unwrap().list.len(), 2); -// assert_eq!(doc.native.write.unwrap().allow.unwrap().list.len(), 1); -// } - -// #[test_log::test] -// pub fn test_native_merge_empty() -> Result<()> { -// let child = NativeTools::default(); -// let parent = NativeTools::default(); -// let merged = child.merge(parent); - -// assert_eq!(merged, NativeTools::default()); -// Ok(()) -// } - -// #[test_log::test] -// pub fn test_native_merge_empty_child() -> Result<()> { -// let child = NativeTools::default(); -// let parent = NativeTools { -// aws: Some(AwsTool { -// disable_auto_readonly: None, -// allow: Some(vec!["ec2"].into_iter().collect()), -// deny: Some(vec!["iam"].into_iter().collect()), -// }), -// shell: Some(ExecuteShellTool { -// allow: Some(vec!["ls .*"].into_iter().collect()), -// deny: Some(vec!["git push"].into_iter().collect()), -// r#override: vec![Override::from("rm -rf /")], -// deny_by_default: Some(true), -// disable_auto_readonly: Some(false), -// }), -// read: Some(ReadTool { -// allow: Some(vec!["ls .*"].into_iter().collect()), -// deny: Some(vec!["git push"].into_iter().collect()), -// r#override: vec![Override::from("rm -rf /")], -// }), -// write: Some(WriteTool { -// allow: Some(vec!["ls .*"].into_iter().collect()), -// deny: Some(vec!["git push"].into_iter().collect()), -// overrides: vec![Override::from("rm -rf /")], -// }), -// }; - -// let merged = child.merge(parent.clone()); -// assert_eq!(merged.aws, parent.aws); -// assert_eq!(merged.shell, parent.shell); -// assert_eq!(merged.read, parent.read); -// assert_eq!(merged.write, parent.write); -// Ok(()) -// } - -// #[test_log::test] -// pub fn test_native_merge_child_parent() -> Result<()> { -// let child = NativeTools { -// aws: Some(AwsTool { -// disable_auto_readonly: Some(true), -// allow: Some(vec!["ec2"].into_iter().collect()), -// deny: None, -// }), -// ..Default::default() -// }; - -// let parent = NativeTools { -// aws: Some(AwsTool { -// disable_auto_readonly: None, -// allow: Some(vec!["ec2"].into_iter().collect()), -// deny: Some(vec!["iam"].into_iter().collect()), -// }), -// ..Default::default() -// }; - -// let merged = child.merge(parent); -// let aws = merged.aws.unwrap(); -// assert!(aws.disable_auto_readonly.unwrap()); -// // Should have deduplicated ec2 -// assert_eq!(aws.allow.unwrap().into_set().len(), 1); -// assert_eq!( -// aws.deny.unwrap().into_set(), -// HashSet::from_iter(vec!["iam".to_string()]) -// ); -// Ok(()) -// } - -// #[test_log::test] -// pub fn test_native_merge_shell() -> Result<()> { -// let child = ExecuteShellTool::default(); -// let parent = ExecuteShellTool { -// deny_by_default: Some(false), -// disable_auto_readonly: Some(false), -// ..Default::default() -// }; - -// let merged = child.clone().merge(parent); -// assert!(!merged.deny_by_default.unwrap()); -// assert!(!merged.disable_auto_readonly.unwrap()); - -// let parent = ExecuteShellTool { -// deny_by_default: Some(true), -// disable_auto_readonly: Some(true), -// ..Default::default() -// }; -// let merged = child.clone().merge(parent); -// assert!(merged.deny_by_default.unwrap()); -// assert!(merged.disable_auto_readonly.unwrap()); - -// let child = ExecuteShellTool { -// deny_by_default: Some(false), -// disable_auto_readonly: Some(false), -// ..Default::default() -// }; -// let parent = ExecuteShellTool { -// deny_by_default: Some(true), -// disable_auto_readonly: Some(true), -// ..Default::default() -// }; -// let merged = child.merge(parent); -// assert!(!merged.deny_by_default.unwrap()); -// assert!(!merged.disable_auto_readonly.unwrap()); -// Ok(()) -// } - -// #[test_log::test] -// pub fn test_native_aws_kiro() -> Result<()> { -// let a = NativeTools::default(); -// let kiro = KiroAwsTool::from(&a); -// assert!(kiro.auto_allow_readonly); -// assert!(kiro.allowed_services.is_empty()); -// assert!(kiro.denied_services.is_empty()); - -// let a = NativeTools { -// aws: Some(AwsTool { -// disable_auto_readonly: Some(true), -// allow: Some("blah".into()), -// deny: Some("blahblah".into()), -// }), -// ..Default::default() -// }; - -// let kiro = KiroAwsTool::from(&a); -// assert!(!kiro.auto_allow_readonly); -// assert!(kiro.allowed_services.contains("blah")); -// assert!(kiro.denied_services.contains("blahblah")); -// assert_eq!(kiro.allowed_services.len() + kiro.denied_services.len(), -// 2); Ok(()) -// } - -// #[test_log::test] -// pub fn test_native_shell_kiro() -> Result<()> { -// let a = NativeTools::default(); -// let kiro = KiroShellTool::from(&a); -// assert!(kiro.auto_allow_readonly); -// assert!(kiro.allowed_commands.is_empty()); -// assert!(kiro.denied_commands.is_empty()); - -// let a = NativeTools { -// shell: Some(ExecuteShellTool { -// allow: Some("ls".into()), -// deny: Some("rm".into()), -// deny_by_default: None, -// disable_auto_readonly: None, -// r#override: vec!["rm".into()], -// }), -// ..Default::default() -// }; -// let kiro = KiroShellTool::from(&a); -// assert!(kiro.auto_allow_readonly); -// assert_eq!(kiro.allowed_commands.len(), 2); -// assert_eq!( -// kiro.allowed_commands, -// HashSet::from_iter(vec!["ls".to_string(), "rm".to_string()]) -// ); -// assert!(kiro.denied_commands.is_empty()); -// Ok(()) -// } - -// #[test_log::test] -// pub fn test_native_read_kiro() -> Result<()> { -// let a = NativeTools::default(); -// let kiro = KiroReadTool::from(&a); -// assert!(kiro.allowed_paths.is_empty()); -// assert!(kiro.denied_paths.is_empty()); - -// let a = NativeTools { -// read: Some(ReadTool { -// allow: Some("ls".into()), -// deny: Some("rm".into()), -// r#override: vec!["rm".into()], -// }), -// ..Default::default() -// }; -// let kiro = KiroReadTool::from(&a); -// assert_eq!(kiro.allowed_paths.len(), 2); -// assert_eq!( -// kiro.allowed_paths, -// HashSet::from_iter(vec!["ls".to_string(), "rm".to_string()]) -// ); -// assert!(kiro.denied_paths.is_empty()); -// Ok(()) -// } - -// #[test_log::test] -// pub fn test_native_write_kiro() -> Result<()> { -// let a = NativeTools::default(); -// let kiro = KiroWriteTool::from(&a); -// assert!(kiro.allowed_paths.is_empty()); -// assert!(kiro.denied_paths.is_empty()); - -// let a = NativeTools { -// write: Some(WriteTool { -// allow: Some("ls".into()), -// deny: Some("rm".into()), -// overrides: vec!["rm".into()], -// }), -// ..Default::default() -// }; -// let kiro = KiroWriteTool::from(&a); -// assert_eq!(kiro.allowed_paths.len(), 2); -// assert_eq!( -// kiro.allowed_paths, -// HashSet::from_iter(vec!["ls".to_string(), "rm".to_string()]) -// ); -// assert!(kiro.denied_paths.is_empty()); -// Ok(()) -// } -// } +#[cfg(test)] +mod tests { + use { + super::*, + crate::{ + Result, + config::{ConfigResult, kdl_parse}, + }, + std::fmt::Display, + }; + fn into_set(v: Vec) -> HashSet { + HashSet::from_iter(v.into_iter().map(|t| t.to_string())) + } + #[test_log::test] + fn parse_shell_tool() -> ConfigResult<()> { + let kdl = r#" + shell deny-by-default=#true disable-auto-readonly=#false { + allow """ + ls .* + git status + """ + deny "rm -rf /" + override "git push" + } + "#; + + let doc: NativeToolsDoc = kdl_parse(kdl)?; + let doc = NativeTools::from(doc); + let shell = doc.shell; + assert_eq!(shell.allows.len(), 2); + assert_eq!(shell.denies.len(), 1); + assert!(shell.deny_by_default.unwrap_or_default()); + assert!(!shell.disable_auto_readonly.unwrap_or_default()); + assert_eq!(shell.overrides.len(), 1); + Ok(()) + } + + #[test_log::test] + fn parse_aws_tool() -> ConfigResult<()> { + let kdl = r#" + aws disable-auto-readonly=#true { + allow "ec2" + allow "s3" + deny "iam" + } + "#; + + let doc: NativeToolsDoc = kdl_parse(kdl)?; + let aws = NativeTools::from(doc).aws; + assert!(aws.disable_auto_readonly.is_some()); + assert!(aws.disable_auto_readonly.unwrap_or_default()); + assert_eq!(aws.allows.len(), 2); + assert_eq!(aws.denies.len(), 1); + Ok(()) + } + + #[test_log::test] + fn parse_read_write_tools() -> ConfigResult<()> { + let kdl = r#" + read { + allow """ + *.rs + *.toml + """ + deny "/etc/*" + override "/etc/hosts" + } + write { + allow "*.txt" + deny "/tmp/*" + override "/tmp/allowed" + } + "#; + + let doc: NativeToolsDoc = kdl_parse(kdl)?; + let doc = NativeTools::from(doc); + assert_eq!(doc.read.allows.len(), 2); + assert_eq!(doc.write.allows.len(), 1); + Ok(()) + } + + #[test_log::test] + pub fn test_native_merge_empty() -> Result<()> { + let child = NativeTools::default(); + let parent = NativeTools::default(); + let merged = child.merge(parent); + + assert_eq!(merged, NativeTools::default()); + Ok(()) + } + + #[test_log::test] + pub fn test_native_merge_empty_child() -> Result<()> { + let child = NativeTools::default(); + let parent = NativeTools { + aws: AwsTool { + disable_auto_readonly: None, + deny_by_default: None, + overrides: Default::default(), + allows: into_set(vec!["ec2"]), + denies: into_set(vec!["iam"]), + }, + shell: ExecuteShellTool { + allows: into_set(vec!["ls .*"]), + denies: into_set(vec!["git push"]), + overrides: into_set(vec!["rm -rf /"]), + deny_by_default: Some(true), + disable_auto_readonly: Some(false), + }, + read: ReadTool { + allows: into_set(vec!["ls .*"]), + denies: into_set(vec!["git push"]), + overrides: into_set(vec!["rm -rf /"]), + ..Default::default() + }, + write: WriteTool { + allows: into_set(vec!["ls .*"]), + denies: into_set(vec!["git push"]), + overrides: into_set(vec!["rm -rf /"]), + ..Default::default() + }, + }; + + let merged = child.merge(parent.clone()); + assert_eq!(merged.aws, parent.aws); + assert_eq!(merged.shell, parent.shell); + assert_eq!(merged.read, parent.read); + assert_eq!(merged.write, parent.write); + Ok(()) + } + + #[test_log::test] + pub fn test_native_merge_child_parent() -> Result<()> { + let child = NativeTools { + aws: AwsTool { + disable_auto_readonly: Some(true), + allows: into_set(vec!["ec2"]), + ..Default::default() + }, + ..Default::default() + }; + + let parent = NativeTools { + aws: AwsTool { + allows: into_set(vec!["ec2"]), + denies: into_set(vec!["iam"]), + ..Default::default() + }, + ..Default::default() + }; + + let merged = child.merge(parent); + let aws = merged.aws; + assert!(aws.disable_auto_readonly.unwrap_or_default()); + // Should have deduplicated ec2 + assert_eq!(aws.allows.len(), 1); + assert_eq!(aws.denies, into_set(vec!["iam".to_string()])); + Ok(()) + } + + #[test_log::test] + pub fn test_native_merge_shell() -> Result<()> { + let child = ExecuteShellTool::default(); + let parent = ExecuteShellTool { + deny_by_default: Some(false), + disable_auto_readonly: Some(false), + ..Default::default() + }; + + let merged = child.clone().merge(parent); + assert!(!merged.deny_by_default.unwrap_or_default()); + assert!(!merged.disable_auto_readonly.unwrap_or_default()); + + let parent = ExecuteShellTool { + deny_by_default: Some(true), + disable_auto_readonly: Some(true), + ..Default::default() + }; + let merged = child.clone().merge(parent); + assert!(merged.deny_by_default.unwrap_or_default()); + assert!(merged.disable_auto_readonly.unwrap_or_default()); + + let child = ExecuteShellTool { + deny_by_default: Some(false), + disable_auto_readonly: Some(false), + ..Default::default() + }; + let parent = ExecuteShellTool { + deny_by_default: Some(true), + disable_auto_readonly: Some(true), + ..Default::default() + }; + let merged = child.merge(parent); + assert!(!merged.deny_by_default.unwrap_or_default()); + assert!(!merged.disable_auto_readonly.unwrap_or_default()); + Ok(()) + } + + #[test_log::test] + pub fn test_native_aws_kiro() -> Result<()> { + let a = NativeTools::default(); + let kiro = KiroAwsTool::from(&a); + assert!(kiro.auto_allow_readonly); + assert!(kiro.allowed_services.is_empty()); + assert!(kiro.denied_services.is_empty()); + + let a = NativeTools { + aws: AwsTool { + disable_auto_readonly: Some(true), + allows: into_set(vec!["blah"]), + denies: into_set(vec!["blahblah"]), + ..Default::default() + }, + ..Default::default() + }; + + let kiro = KiroAwsTool::from(&a); + assert!(!kiro.auto_allow_readonly); + assert!(kiro.allowed_services.contains("blah")); + assert!(kiro.denied_services.contains("blahblah")); + assert_eq!(kiro.allowed_services.len() + kiro.denied_services.len(), 2); + Ok(()) + } + + #[test_log::test] + pub fn test_native_shell_kiro() -> Result<()> { + let a = NativeTools::default(); + let kiro = KiroShellTool::from(&a); + assert!(kiro.auto_allow_readonly); + assert!(kiro.allowed_commands.is_empty()); + assert!(kiro.denied_commands.is_empty()); + + let a = NativeTools { + shell: ExecuteShellTool { + allows: into_set(vec!["ls"]), + denies: into_set(vec!["rm"]), + deny_by_default: None, + disable_auto_readonly: None, + overrides: into_set(vec!["rm"]), + }, + ..Default::default() + }; + let kiro = KiroShellTool::from(&a); + assert!(kiro.auto_allow_readonly); + assert_eq!(kiro.allowed_commands.len(), 2); + assert_eq!( + kiro.allowed_commands, + into_set(vec!["ls".to_string(), "rm".to_string()]) + ); + assert!(kiro.denied_commands.is_empty()); + Ok(()) + } + + #[test_log::test] + pub fn test_native_read_kiro() -> Result<()> { + let a = NativeTools::default(); + let kiro = KiroReadTool::from(&a); + assert!(kiro.allowed_paths.is_empty()); + assert!(kiro.denied_paths.is_empty()); + + let a = NativeTools { + read: ReadTool { + allows: into_set(vec!["ls"]), + denies: into_set(vec!["rm"]), + overrides: into_set(vec!["rm"]), + ..Default::default() + }, + ..Default::default() + }; + let kiro = KiroReadTool::from(&a); + assert_eq!(kiro.allowed_paths.len(), 2); + assert_eq!( + kiro.allowed_paths, + into_set(vec!["ls".to_string(), "rm".to_string()]) + ); + assert!(kiro.denied_paths.is_empty()); + Ok(()) + } + + #[test_log::test] + pub fn test_native_write_kiro() -> Result<()> { + let a = NativeTools::default(); + let kiro = KiroWriteTool::from(&a); + assert!(kiro.allowed_paths.is_empty()); + assert!(kiro.denied_paths.is_empty()); + let write = WriteTool { + allows: into_set(vec!["ls"]), + denies: into_set(vec!["rm"]), + overrides: into_set(vec!["rm"]), + ..Default::default() + }; + let a = NativeTools { + write, + ..Default::default() + }; + + let kiro = KiroWriteTool::from(&a); + assert_eq!(kiro.allowed_paths.len(), 2); + assert_eq!( + kiro.allowed_paths, + into_set(vec!["ls".to_string(), "rm".to_string()]) + ); + assert!(kiro.denied_paths.is_empty()); + Ok(()) + } +} diff --git a/src/generator/discover.rs b/src/generator/discover.rs index bf15f7b..89e59a3 100644 --- a/src/generator/discover.rs +++ b/src/generator/discover.rs @@ -94,12 +94,12 @@ pub fn discover( let local_path = location.local_kg(); let global_agents: GeneratorConfig = load_inline(fs, global_path)?; let local_agents: GeneratorConfig = load_inline(fs, local_path)?; - tracing::debug!("found {} local agents", local_agents.agent.len()); + tracing::debug!("found {} local agents", local_agents.agents.len()); let local_names: HashSet = - HashSet::from_iter(local_agents.agent.keys().map(|k| k.to_string())); + HashSet::from_iter(local_agents.agents.keys().map(|k| k.to_string())); let global_names: HashSet = - HashSet::from_iter(global_agents.agent.keys().map(|k| k.to_string())); + HashSet::from_iter(global_agents.agents.keys().map(|k| k.to_string())); let mut all_agents_names: HashSet = HashSet::with_capacity(global_names.len() + local_names.len()); all_agents_names.extend(local_names.clone()); diff --git a/src/kdl/agent.rs b/src/kdl/agent.rs deleted file mode 100644 index b2a1331..0000000 --- a/src/kdl/agent.rs +++ /dev/null @@ -1,244 +0,0 @@ -use { - super::{hook::HookPart, mcp::CustomToolConfigKdl}, - crate::{ - agent::{ - CustomToolConfig, - OriginalToolName, - hook::{Hook, HookTrigger}, - }, - kdl::native::{AwsTool, ExecuteShellTool, NativeTools, ReadTool, WriteTool}, - }, - color_eyre::eyre::WrapErr, - knuffel::Decode, - std::{ - collections::{HashMap, HashSet}, - fmt::{Debug, Display}, - hash::Hash, - }, -}; - -#[derive(Decode, Clone, Default, Debug)] -pub(super) struct Inherits { - #[knuffel(arguments, default)] - pub parents: HashSet, -} - -#[derive(Decode, Clone, Default, Debug)] -pub(super) struct Tools { - #[knuffel(arguments, default)] - pub tools: HashSet, -} - -#[derive(Decode, Clone, Default, Debug)] -pub(super) struct AllowedTools { - #[knuffel(arguments, default)] - pub allowed: HashSet, -} - -#[derive(Decode, Clone, Default, Debug)] -pub(super) struct Resource { - #[knuffel(argument)] - pub location: String, -} - -impl PartialEq for Resource { - fn eq(&self, other: &Self) -> bool { - self.location.eq(&other.location) - } -} - -impl Hash for Resource { - fn hash(&self, state: &mut H) { - self.location.hash(state); - } -} -impl Eq for Resource {} - -#[derive(Decode, Clone, Default, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub(super) struct ToolAliasKdl { - #[knuffel(argument)] - from: String, - #[knuffel(argument)] - to: String, -} - -/// Raw JSON tool settings for forward compatibility. -/// -/// Allows users to configure tool settings not yet supported by kg's schema. -/// The JSON must be a valid object (not array or primitive). -/// -/// See https://kiro.dev/docs/cli/custom-agents/configuration-reference/#toolssettings-field -#[derive(Decode, Clone, Debug)] -pub struct ToolSetting { - #[knuffel(argument)] - name: String, - #[knuffel(child, unwrap(argument))] - json: String, -} - -impl ToolSetting { - fn to_value(&self) -> crate::Result<(String, serde_json::Value)> { - let v: serde_json::Value = serde_json::from_str(&self.json) - .wrap_err_with(|| format!("Failed to parse JSON for tool-setting '{}'", self.name))?; - - if !v.is_object() { - return Err(color_eyre::eyre::eyre!( - "tool-setting '{}' must be a JSON object, got: {}", - self.name, - v - )); - } - - Ok((self.name.clone(), v)) - } -} - -#[derive(Decode, Clone, Default)] -pub struct KdlAgent { - /// Name of the agent - #[knuffel(argument)] - pub name: String, - /// Do not generate JSON Kiro agent, this is a "template" agent - #[knuffel(property, default)] - pub template: Option, - #[knuffel(child, unwrap(argument))] - pub description: Option, - #[knuffel(child, default)] - pub(super) inherits: Inherits, - /// The intention for this field is to provide high level context to the - /// agent. This should be seen as the same category of context as a system - /// prompt. - #[knuffel(child, unwrap(argument))] - pub prompt: Option, - /// Files to include in the agent's context - #[knuffel(children(name = "resource"))] - pub(super) resources: HashSet, - #[knuffel(child, default, unwrap(argument))] - pub include_mcp_json: Option, - /// List of tools the agent can see. Use \"@{MCP_SERVER_NAME}/tool_name\" to - /// specify tools from mcp servers. To include all tools from a server, - /// use \"@{MCP_SERVER_NAME}\" - #[knuffel(child, default)] - pub(super) tools: Tools, - /// List of tools the agent is explicitly allowed to use - #[knuffel(child, default)] - pub(super) allowed_tools: AllowedTools, - /// The model ID to use for this agent. If not specified, uses the default - /// model. - #[knuffel(child, unwrap(argument))] - pub model: Option, - /// Commands to run when a chat session is created - #[knuffel(child)] - pub(super) hook: Option, - #[knuffel(children(name = "mcp"), default)] - pub(super) mcp: Vec, - #[knuffel(children(name = "alias"), default)] - pub(super) tool_aliases: HashSet, - /// Tools builtin to kiro - #[knuffel(child, default)] - pub native_tool: NativeTools, - - #[knuffel(children(name = "tool-setting"), default)] - pub(super) tool_settings: Vec, -} - -impl Debug for KdlAgent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.name) - } -} - -impl Display for KdlAgent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.name) - } -} - -impl KdlAgent { - pub fn new(name: impl AsRef) -> Self { - Self { - name: name.as_ref().to_string(), - ..Default::default() - } - } - - pub fn is_template(&self) -> bool { - self.template.is_some_and(|f| f) - } - - pub fn include_mcp_json(&self) -> bool { - self.include_mcp_json.is_some_and(|f| f) - } - - pub fn get_tool_aws(&self) -> &AwsTool { - &self.native_tool.aws - } - - pub fn get_tool_read(&self) -> &ReadTool { - &self.native_tool.read - } - - pub fn get_tool_write(&self) -> &WriteTool { - &self.native_tool.write - } - - pub fn get_tool_shell(&self) -> &ExecuteShellTool { - &self.native_tool.shell - } - - pub fn tool_aliases(&self) -> HashMap { - self.tool_aliases - .iter() - .map(|m| (OriginalToolName(m.from.clone()), m.to.clone())) - .collect() - } - - pub fn hooks(&self) -> HashMap> { - match &self.hook { - None => HashMap::new(), - Some(h) => h.triggers(), - } - } - - pub fn allowed_tools(&self) -> &HashSet { - &self.allowed_tools.allowed - } - - pub fn tools(&self) -> &HashSet { - &self.tools.tools - } - - pub fn inherits(&self) -> &HashSet { - &self.inherits.parents - } - - pub fn resources(&self) -> impl Iterator { - self.resources.iter().map(|r| r.location.as_str()) - } - - pub fn mcp_servers(&self) -> HashMap { - self.mcp - .iter() - .map(|m| (m.name.clone(), m.into())) - .collect() - } - - /// Parse raw JSON tool settings into a map. - /// - /// This allows users to configure tools not yet supported by kg's schema. - pub fn extra_tool_settings(&self) -> crate::Result> { - let mut result = HashMap::new(); - for setting in &self.tool_settings { - let (name, value) = setting.to_value()?; - if result.contains_key(&name) { - return Err(color_eyre::eyre::eyre!( - "[{self}] - Duplicate tool-setting '{}' found. Each tool-setting name must be \ - unique.", - name - )); - } - result.insert(name, value); - } - Ok(result) - } -} diff --git a/src/kdl/agent_file.rs b/src/kdl/agent_file.rs deleted file mode 100644 index 0f6edc5..0000000 --- a/src/kdl/agent_file.rs +++ /dev/null @@ -1,78 +0,0 @@ -use { - super::{agent::*, hook::HookPart, mcp::CustomToolConfigKdl, native::NativeTools}, - crate::os::Fs, - color_eyre::eyre::eyre, - knuffel::{Decode, parse}, - std::{collections::HashSet, path::Path}, -}; - -#[derive(Decode, Clone, Debug)] -pub struct KdlAgentFileSource { - #[knuffel(child, unwrap(argument))] - pub description: Option, - #[knuffel(child, unwrap(argument))] - pub prompt: Option, - #[knuffel(children(name = "resource"))] - pub(super) resources: HashSet, - #[knuffel(child, default, unwrap(argument))] - pub include_mcp_json: Option, - #[knuffel(child, default)] - pub(super) tools: Tools, - #[knuffel(child, default)] - pub(super) allowed_tools: AllowedTools, - #[knuffel(child, unwrap(argument))] - pub model: Option, - #[knuffel(child)] - pub(super) hook: Option, - #[knuffel(children(name = "mcp"), default)] - pub(super) mcp: Vec, - #[knuffel(children(name = "alias"), default)] - pub(super) tool_aliases: HashSet, - #[knuffel(child, default)] - pub(super) native_tool: NativeTools, - #[knuffel(children(name = "tool-setting"), default)] - pub(super) tool_settings: Vec, -} - -impl KdlAgent { - pub fn from_path( - fs: &Fs, - name: impl AsRef, - path: impl AsRef, - ) -> crate::Result> { - if !fs.exists(&path) { - return Ok(None); - } - - let content = fs.read_to_string_sync(&path)?; - let path_str = format!("{}", path.as_ref().display()); - let agent: KdlAgentFileSource = match parse(&path_str, &content) { - Ok(a) => a, - Err(e) => { - eprintln!("{:?}", miette::Report::new(e)); - return Err(eyre!("failed to parse agent file")); - } - }; - Ok(Some(Self::from_file_source(name, agent))) - } - - pub fn from_file_source(name: impl AsRef, file_source: KdlAgentFileSource) -> Self { - Self { - name: name.as_ref().to_string(), - description: file_source.description, - template: None, - inherits: Inherits::default(), - prompt: file_source.prompt, - resources: file_source.resources, - include_mcp_json: file_source.include_mcp_json, - tools: file_source.tools, - allowed_tools: file_source.allowed_tools, - model: file_source.model, - hook: file_source.hook, - mcp: file_source.mcp, - tool_aliases: file_source.tool_aliases, - native_tool: file_source.native_tool, - tool_settings: file_source.tool_settings, - } - } -} diff --git a/src/kdl/hook.rs b/src/kdl/hook.rs deleted file mode 100644 index 92edaac..0000000 --- a/src/kdl/hook.rs +++ /dev/null @@ -1,348 +0,0 @@ -use { - crate::agent::hook::{Hook, HookTrigger}, - knuffel::Decode, - std::collections::HashMap, -}; - -macro_rules! define_hook { - ($name:ident) => { - #[derive(Decode, Default, Clone, Debug, PartialEq, Eq)] - pub(super) struct $name { - /// The command to run when the hook is triggered - #[knuffel(argument)] - pub name: String, - /// The command to run when the hook is triggered - #[knuffel(child, default, unwrap(argument))] - pub command: String, - - /// Max time the hook can run before it throws a timeout error - #[knuffel(child, default, unwrap(argument))] - pub timeout_ms: u64, - - /// Max output size of the hook before it is truncated - #[knuffel(child, default, unwrap(argument))] - pub max_output_size: usize, - - /// How long the hook output is cached before it will be executed again - #[knuffel(child, default, unwrap(argument))] - pub cache_ttl_seconds: u64, - - /// Optional glob matcher for hook - /// Currently used for matching tool name of PreToolUse and PostToolUse hook - #[knuffel(child, default, unwrap(argument))] - pub matcher: Option, - } - - impl From<$name> for Hook { - fn from(value: $name) -> Hook { - Hook { - command: value.command, - timeout_ms: value.timeout_ms, - max_output_size: value.max_output_size, - cache_ttl_seconds: value.cache_ttl_seconds, - matcher: value.matcher, - } - } - } - - impl $name { - fn merge(mut self, o: $name) -> $name { - if self.cache_ttl_seconds == 0 && o.cache_ttl_seconds > 0 { - self.cache_ttl_seconds = o.cache_ttl_seconds; - } - if self.command.is_empty() { - self.command = o.command.clone(); - } - if self.max_output_size == 0 && o.max_output_size > 0 { - self.max_output_size = o.max_output_size; - } - if self.timeout_ms == 0 && o.timeout_ms > 0 { - self.timeout_ms = o.timeout_ms; - } - if self.matcher.is_none() - && let Some(m) = &o.matcher - { - self.matcher = Some(m.clone()); - } - self - } - } - }; -} - -define_hook!(HookAgentSpawn); -define_hook!(HookUserPromptSubmit); -define_hook!(HookPreToolUse); -define_hook!(HookPostToolUse); -define_hook!(HookStop); - -#[derive(Decode, Clone, Default, Debug, PartialEq, Eq)] -pub(super) struct HookPart { - #[knuffel(children(name = "agent-spawn"), default)] - pub agent_spawn: Vec, - #[knuffel(children(name = "user-prompt-submit"), default)] - pub user_prompt_submit: Vec, - #[knuffel(children(name = "pre-tool-use"), default)] - pub pre_tool_use: Vec, - #[knuffel(children(name = "post-tool-use"), default)] - pub post_tool_use: Vec, - #[knuffel(children(name = "stop"), default)] - pub stop: Vec, -} - -impl HookPart { - pub fn merge(mut self, other: Self) -> Self { - match (self.agent_spawn.is_empty(), other.agent_spawn.is_empty()) { - (false, false) => { - let mut hooks = Vec::with_capacity(self.agent_spawn.len()); - for h in self.agent_spawn { - if let Some(o) = other.agent_spawn.iter().find(|i| i.name == h.name) { - hooks.push(h.merge(o.clone())); - } else { - hooks.push(h); - } - } - self.agent_spawn = hooks; - for o in other.agent_spawn.into_iter() { - if !self.agent_spawn.iter().any(|h| h.name == o.name) { - self.agent_spawn.push(o); - } - } - } - (true, false) => self.agent_spawn = other.agent_spawn, - _ => {} - }; - - match ( - self.user_prompt_submit.is_empty(), - other.user_prompt_submit.is_empty(), - ) { - (false, false) => { - let mut hooks = Vec::with_capacity(self.user_prompt_submit.len()); - for h in self.user_prompt_submit { - if let Some(o) = other.user_prompt_submit.iter().find(|i| i.name.eq(&h.name)) { - hooks.push(h.merge(o.clone())); - } else { - hooks.push(h); - } - } - self.user_prompt_submit = hooks; - for o in other.user_prompt_submit.into_iter() { - if !self.user_prompt_submit.iter().any(|h| h.name == o.name) { - self.user_prompt_submit.push(o); - } - } - } - (true, false) => self.user_prompt_submit = other.user_prompt_submit, - _ => {} - }; - - match (self.pre_tool_use.is_empty(), other.pre_tool_use.is_empty()) { - (false, false) => { - let mut hooks = Vec::with_capacity(self.pre_tool_use.len()); - for h in self.pre_tool_use { - if let Some(o) = other.pre_tool_use.iter().find(|i| i.name.eq(&h.name)) { - hooks.push(h.merge(o.clone())); - } else { - hooks.push(h); - } - } - self.pre_tool_use = hooks; - for o in other.pre_tool_use.into_iter() { - if !self.pre_tool_use.iter().any(|h| h.name == o.name) { - self.pre_tool_use.push(o); - } - } - } - (true, false) => self.pre_tool_use = other.pre_tool_use, - _ => {} - }; - - match ( - self.post_tool_use.is_empty(), - other.post_tool_use.is_empty(), - ) { - (false, false) => { - let mut hooks = Vec::with_capacity(self.post_tool_use.len()); - for h in self.post_tool_use { - if let Some(o) = other.post_tool_use.iter().find(|i| i.name.eq(&h.name)) { - hooks.push(h.merge(o.clone())); - } else { - hooks.push(h); - } - } - self.post_tool_use = hooks; - for o in other.post_tool_use.into_iter() { - if !self.post_tool_use.iter().any(|h| h.name == o.name) { - self.post_tool_use.push(o); - } - } - } - (true, false) => self.post_tool_use = other.post_tool_use, - _ => {} - }; - - match (self.stop.is_empty(), other.stop.is_empty()) { - (false, false) => { - let mut hooks = Vec::with_capacity(self.stop.len()); - for h in self.stop { - if let Some(o) = other.stop.iter().find(|i| i.name.eq(&h.name)) { - hooks.push(h.merge(o.clone())); - } else { - hooks.push(h); - } - } - self.stop = hooks; - for o in other.stop.into_iter() { - if !self.stop.iter().any(|h| h.name == o.name) { - self.stop.push(o); - } - } - } - (true, false) => self.stop = other.stop, - _ => {} - }; - self - } - - pub fn triggers(&self) -> HashMap> { - let trigger: Vec = enum_iterator::all::().collect(); - let mut hooks: HashMap> = HashMap::new(); - for t in trigger { - match t { - HookTrigger::AgentSpawn => { - hooks.insert( - t, - self.agent_spawn - .iter() - .map(|h| Hook::from(h.clone())) - .collect(), - ); - } - HookTrigger::UserPromptSubmit => { - hooks.insert( - t, - self.user_prompt_submit - .iter() - .map(|h| Hook::from(h.clone())) - .collect(), - ); - } - HookTrigger::PreToolUse => { - hooks.insert( - t, - self.pre_tool_use - .iter() - .map(|h| Hook::from(h.clone())) - .collect(), - ); - } - HookTrigger::PostToolUse => { - hooks.insert( - t, - self.post_tool_use - .iter() - .map(|h| Hook::from(h.clone())) - .collect(), - ); - } - HookTrigger::Stop => { - hooks.insert(t, self.stop.iter().map(|h| Hook::from(h.clone())).collect()); - } - }; - } - hooks - } -} - -#[cfg(test)] -mod tests { - use {super::*, crate::Result, std::time::Duration}; - - macro_rules! rando_hook { - ($name:ident) => { - impl $name { - fn rando() -> $name { - let value = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - Self { - name: format!("$name-{value}"), - command: format!("{value}"), - timeout_ms: value, - max_output_size: 0, - cache_ttl_seconds: value, - matcher: Some(format!("{value}")), - } - } - } - }; - } - rando_hook!(HookAgentSpawn); - rando_hook!(HookUserPromptSubmit); - rando_hook!(HookPreToolUse); - rando_hook!(HookPostToolUse); - rando_hook!(HookStop); - - impl HookPart { - pub fn randomize() -> Self { - Self { - agent_spawn: vec![HookAgentSpawn::rando()], - user_prompt_submit: vec![HookUserPromptSubmit::rando()], - pre_tool_use: vec![HookPreToolUse::rando()], - post_tool_use: vec![HookPostToolUse::rando()], - stop: vec![HookStop::rando()], - } - } - } - - #[test_log::test] - pub fn test_hooks_empty() -> Result<()> { - let child = HookPart::default(); - let parent = HookPart::default(); - let merged = child.merge(parent); - - assert!(merged.agent_spawn.is_empty()); - assert!(merged.user_prompt_submit.is_empty()); - assert!(merged.pre_tool_use.is_empty()); - assert!(merged.post_tool_use.is_empty()); - assert!(merged.stop.is_empty()); - Ok(()) - } - - #[test_log::test] - pub fn test_hooks_empty_child() -> Result<()> { - let child = HookPart::default(); - let parent = HookPart::randomize(); - let before = parent.clone(); - let merged = child.merge(parent); - - assert_eq!(merged, before); - Ok(()) - } - - #[test_log::test] - pub fn test_hooks_no_merge() -> Result<()> { - let child = HookPart::randomize(); - let parent = HookPart::randomize(); - let before = child.clone(); - let merged = child.merge(parent); - assert_eq!(merged, before); - Ok(()) - } - - #[test_log::test] - pub fn test_hooks_merge_parent() -> Result<()> { - let child = HookPart::randomize(); - std::thread::sleep(Duration::from_millis(1300)); // see randomize function - let parent = HookPart::randomize(); - let merged = child.merge(parent); - assert_eq!(merged.agent_spawn.len(), 2); - assert_eq!(merged.user_prompt_submit.len(), 2); - assert_eq!(merged.pre_tool_use.len(), 2); - assert_eq!(merged.post_tool_use.len(), 2); - assert_eq!(merged.stop.len(), 2); - Ok(()) - } -} diff --git a/src/kdl/mcp.rs b/src/kdl/mcp.rs deleted file mode 100644 index 161a80a..0000000 --- a/src/kdl/mcp.rs +++ /dev/null @@ -1,96 +0,0 @@ -use {crate::agent::CustomToolConfig, knuffel::Decode}; - -#[derive(Decode, Clone, Debug)] -struct EnvVar { - #[knuffel(argument)] - key: String, - #[knuffel(argument)] - value: String, -} - -#[derive(Decode, Clone, Debug)] -struct Header { - #[knuffel(argument)] - key: String, - #[knuffel(argument)] - value: String, -} - -#[derive(Decode, Default, Clone, Debug)] -struct ToolArgs { - #[knuffel(arguments, default)] - args: Vec, -} - -#[derive(Decode, Clone, Debug, Eq, PartialEq)] -pub struct OAuthConfig { - /// Custom redirect URI for OAuth flow (e.g., "127.0.0.1:7778") - /// If not specified, a random available port will be assigned by the OS - #[knuffel(child, unwrap(argument))] - pub redirect_uri: String, -} - -#[derive(Decode, Clone, Debug)] -pub struct CustomToolConfigKdl { - #[knuffel(argument)] - pub name: String, - - #[knuffel(child, unwrap(argument))] - pub url: Option, - - #[knuffel(child, unwrap(argument))] - pub command: Option, - - #[knuffel(child)] - pub oauth: Option, - - #[knuffel(child, default)] - args: ToolArgs, - - #[knuffel(children(name = "env"))] - env_vars: Vec, - - #[knuffel(children(name = "header"))] - headers: Vec
, - - #[knuffel(child, default, unwrap(argument))] - pub timeout: u64, - - #[knuffel(child, default, unwrap(argument))] - pub disabled: bool, -} - -impl From for CustomToolConfig { - fn from(value: CustomToolConfigKdl) -> Self { - let command = value.command.unwrap_or_default(); - let url = value.url.unwrap_or_default(); - - Self { - url, - command, - args: value.args.args, - timeout: if value.timeout == 0 { - crate::agent::tool_default_timeout() - } else { - value.timeout - }, - disabled: value.disabled, - headers: value - .headers - .into_iter() - .map(|h| (h.key, h.value)) - .collect(), - env: value - .env_vars - .into_iter() - .map(|e| (e.key, e.value)) - .collect(), - } - } -} - -impl From<&CustomToolConfigKdl> for CustomToolConfig { - fn from(value: &CustomToolConfigKdl) -> Self { - value.clone().into() - } -} diff --git a/src/kdl/merge.rs b/src/kdl/merge.rs deleted file mode 100644 index 98a54bf..0000000 --- a/src/kdl/merge.rs +++ /dev/null @@ -1,207 +0,0 @@ -use super::*; - -impl KdlAgent { - pub fn merge(mut self, other: KdlAgent) -> Self { - // Child wins for explicit values - self.include_mcp_json = self.include_mcp_json.or(other.include_mcp_json); - self.template = self.template.or(other.template); - self.description = self.description.or(other.description); - self.prompt = self.prompt.or(other.prompt); - self.model = self.model.or(other.model); - - // Collections are extended (merged) - self.resources.extend(other.resources); - self.tools.tools.extend(other.tools.tools); - self.allowed_tools - .allowed - .extend(other.allowed_tools.allowed); - self.tool_aliases.extend(other.tool_aliases); - self.mcp.extend(other.mcp); - self.inherits.parents.extend(other.inherits.parents); - self.tool_settings.extend(other.tool_settings); - - // Hooks are deep merged - self.hook = match (self.hook, other.hook) { - (None, Some(h)) => Some(h), - (Some(a), Some(b)) => Some(a.merge(b)), - (Some(a), None) => Some(a), - (None, None) => None, - }; - - // Native tools are deep merged - self.native_tool = self.native_tool.merge(other.native_tool); - - self - } -} - -// #[cfg(test)] -// mod tests { -// use {super::*, crate::agent::hook::HookTrigger, color_eyre::eyre::eyre, -// knuffel::parse}; - -// #[test_log::test] -// fn test_agent_merge() -> crate::Result<()> { -// let kdl_agents = r#" -// agent "child" template=$false { -// description "I am a child" -// resource "file://child.md" -// resource "file://README.md" -// inherits "parent" -// include-mcp-json true -// tools "@awsdocs" "shell" -// native-tool { -// write { -// override "Cargo.lock" -// } -// shell { -// override "git push .*" -// } -// } -// hook { -// agent-spawn "spawn" { -// command "echo i have spawned" -// max-output-size 9000 -// cache-ttl-seconds 2 -// } -// } -// alias "execute_bash" "shell" -// } -// agent "parent" template=#true { -// description "I am parent" -// resource "file://parent.md" -// resource "file://README.md" -// tools "web_search" "shell" -// prompt "i tell you what to do" -// model "claude" -// allowed-tools "write" -// alias "execute_bash" "shell" -// alias "fs_read" "read" -// native-tool { -// read { -// allow "./src/*" "./scripts/**" -// deny "Cargo.lock" -// } -// write { -// allow "./src/*" "./scripts/**" -// deny "Cargo.lock" -// } - -// shell { -// allow "git status .*" "git pull .*" -// deny "git push .*" -// } -// } -// hook { -// agent-spawn "spawn" { -// timeout-ms 1111 -// } -// user-prompt-submit "submit" { -// command "echo user submitted" -// timeout-ms 1000 -// } -// pre-tool-use "pre" { -// command "echo before tool" -// matcher "git.*" -// } -// post-tool-use "post" { -// command "echo after tool" -// } -// stop "stop" { -// command "echo stopped" -// } -// } -// } -// "#; - -// let config: GeneratorConfig = match parse("example.kdl", kdl_agents) -// { Ok(c) => c, -// Err(e) => { -// eprintln!("{:?}", miette::Report::new(e)); -// return Err(eyre!("failed to parse {kdl_agents}")); -// } -// }; -// assert_eq!(config.agents.len(), 2); -// let child = config -// .agents -// .iter() -// .find(|a| a.name == "child") -// .unwrap() -// .clone(); -// let parent = config -// .agents -// .iter() -// .find(|a| a.name == "parent") -// .unwrap() -// .clone(); -// let merged = child.merge(parent); -// assert!(merged.description.is_some()); -// let d = merged.description.clone().unwrap(); -// assert_eq!(d, "I am a child"); - -// assert_eq!(merged.resources.len(), 3); -// assert!(!merged.is_template()); -// assert!(merged.include_mcp_json()); - -// assert_eq!(merged.inherits.parents.len(), 1); -// assert!(merged.inherits.parents.contains("parent")); - -// assert_eq!(merged.prompt, Some("i tell you what to do".to_string())); -// let tools = merged.tools(); -// assert_eq!(tools.len(), 3); -// assert!(tools.contains("@awsdocs")); -// assert!(tools.contains("shell")); -// assert!(tools.contains("web_search")); - -// assert_eq!(merged.model, Some("claude".to_string())); - -// let allowed_tools = merged.allowed_tools(); -// assert_eq!(allowed_tools.len(), 1); -// assert!(allowed_tools.contains("write")); - -// let hooks = merged.hooks(); -// assert!(!hooks.is_empty()); -// let h = hooks.get(&HookTrigger::AgentSpawn); -// assert!(h.is_some()); -// let h = h.unwrap(); -// assert!(!h.is_empty()); -// assert_eq!(h[0].timeout_ms, 1111); -// assert_eq!(h[0].command, "echo i have spawned"); - -// let h = hooks.get(&HookTrigger::UserPromptSubmit); -// assert!(h.is_some()); -// let h = h.unwrap(); -// assert!(!h.is_empty()); -// assert_eq!(h[0].command, "echo user submitted"); -// assert_eq!(h[0].timeout_ms, 1000); - -// let alias = merged.tool_aliases(); -// assert_eq!(alias.len(), 2); -// assert!(alias.contains_key("fs_read")); -// assert!(alias.contains_key("execute_bash")); - -// let tool = merged.get_tool_write(); -// assert!(tool.override_path.contains(&"Cargo.lock".into())); -// assert_eq!(tool.allow.list.len(), 2); -// assert_eq!(tool.override_path.len(), 1); -// assert_eq!(tool.deny.list.len(), 1); - -// let tool = merged.get_tool_read(); -// assert_eq!(tool.allow.list.len(), 2); -// assert_eq!(tool.override_path.len(), 0); -// assert_eq!(tool.deny.list.len(), 1); - -// let tool = merged.get_tool_shell(); -// assert_eq!(tool.allow.list.len(), 2); -// assert_eq!(tool.override_command.len(), 1); -// assert_eq!(tool.deny.list.len(), 1); - -// let tool = merged.get_tool_aws(); -// assert!(tool.allow.list.is_empty()); -// assert!(tool.deny.list.is_empty()); - -// assert_eq!("child", format!("{merged}")); -// assert_eq!("child", format!("{merged:?}")); -// Ok(()) -// } -// } diff --git a/src/kdl/mod.rs b/src/kdl/mod.rs deleted file mode 100644 index 7a56a37..0000000 --- a/src/kdl/mod.rs +++ /dev/null @@ -1,292 +0,0 @@ -mod agent; -mod agent_file; -mod hook; -mod mcp; -mod merge; -mod native; -use std::{collections::HashSet, fmt::Debug}; - -pub use agent::KdlAgent; - -#[derive(knuffel::Decode, Default)] -pub struct GeneratorConfig { - #[knuffel(children(name = "agent"))] - pub agents: Vec, -} - -impl Debug for GeneratorConfig { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "agents={}", self.agents.len()) - } -} - -impl GeneratorConfig { - pub fn names(&self) -> HashSet { - self.agents.iter().map(|a| a.name.clone()).collect() - } - - pub fn get(&self, name: impl AsRef) -> Option<&KdlAgent> { - self.agents.iter().find(|a| a.name.eq(name.as_ref())) - } -} - -// #[cfg(test)] -// mod tests { -// use { -// super::*, -// crate::{agent::hook::HookTrigger, kdl::agent_file::KdlAgentFileSource}, -// color_eyre::eyre::eyre, -// knuffel::parse, -// }; - -// #[test_log::test] -// fn test_agent_decoding() -> crate::Result<()> { -// let kdl_agents = r#" -// agent "test" { -// inherits "parent" -// description "This is a test agent" -// prompt "Generate a test prompt" -// resource "file://resource.md" -// resource "file://README.md" -// include-mcp-json true -// tools "*" - -// allowed-tools "@awsdocs" -// hook { -// agent-spawn "spawn" { -// command "echo i have spawned" -// timeout-ms 1000 -// max-output-size 9000 -// cache-ttl-seconds 2 -// } -// user-prompt-submit "submit" { -// command "echo user submitted" -// } -// pre-tool-use "pre" { -// command "echo before tool" -// matcher "git.*" -// } -// post-tool-use "post" { -// command "echo after tool" -// } -// stop "stop" { -// command "echo stopped" -// } -// } - -// mcp "awsdocs" { -// command "aws-docs" -// args "--verbose" "--config=/path" -// env "RUST_LOG" "debug" -// env "PATH" "/usr/bin" -// header "Authorization" "Bearer token" -// timeout 5000 -// oauth { -// redirect-uri "127.0.0.1:7778" -// } -// } - -// alias "execute_bash" "shell" - -// native-tool { -// write { -// allow "./src/*" "./scripts/**" -// deny "Cargo.lock" -// override "/tmp" -// override "/var/log" -// } -// shell deny-by-default=#true { -// allow "git status .*" -// deny "git push .*" -// override "git pull .*" -// } -// } - -// tool-setting "@git/status" { -// json "{ \"git_user\": \"$GIT_USER\" }" -// } -// } -// "#; - -// let config: GeneratorConfig = match parse("example.kdl", kdl_agents) { -// Ok(c) => c, -// Err(e) => { -// eprintln!("{:?}", miette::Report::new(e)); -// return Err(eyre!("failed to parse {kdl_agents}")); -// } -// }; -// assert_eq!(config.agents.len(), 1); -// let agent = config.agents[0].clone(); -// assert_eq!(agent.name, "test"); -// assert!(agent.model.is_none()); -// assert!(!agent.is_template()); -// let inherits = agent.inherits(); -// assert_eq!(inherits.len(), 1); -// assert_eq!(inherits.iter().next().unwrap(), "parent"); -// assert!(agent.description.is_some()); -// assert!(agent.prompt.is_some()); -// assert!(agent.include_mcp_json()); -// let tools = agent.tools(); -// assert_eq!(tools.len(), 1); -// assert_eq!(tools.iter().next().unwrap(), "*"); -// let resources: Vec = agent.resources().map(|s| s.to_string()).collect(); -// assert_eq!(resources.len(), 2); -// assert!(resources.contains(&"file://resource.md".to_string())); -// assert!(resources.contains(&"file://README.md".to_string())); - -// let hooks = agent.hooks(); -// assert!(!hooks.is_empty()); -// let hook = hooks.get(&HookTrigger::AgentSpawn); -// assert!(hook.is_some()); -// assert_eq!(hook.unwrap()[0].command, "echo i have spawned"); - -// assert!(hooks.contains_key(&HookTrigger::PreToolUse)); -// assert!(hooks.contains_key(&HookTrigger::PostToolUse)); -// assert!(hooks.contains_key(&HookTrigger::Stop)); -// assert!(hooks.contains_key(&HookTrigger::UserPromptSubmit)); - -// let allowed = agent.allowed_tools(); -// assert_eq!(allowed.len(), 1); -// assert_eq!(allowed.iter().next().unwrap(), "@awsdocs"); - -// let mcp = agent.mcp_servers(); -// assert_eq!(mcp.len(), 1); -// assert!(mcp.contains_key("awsdocs")); -// let aws_docs = mcp.get("awsdocs").unwrap(); -// assert_eq!(aws_docs.command, "aws-docs"); -// assert_eq!(aws_docs.args, vec!["--verbose", "--config=/path"]); -// assert!(!aws_docs.disabled); -// assert_eq!(aws_docs.headers.len(), 1); -// assert_eq!(aws_docs.env.len(), 2); -// assert_eq!(aws_docs.timeout, 5000); -// assert_eq!(agent.tool_aliases().len(), 1); - -// let extra = agent.extra_tool_settings()?; -// assert_eq!(extra.len(), 1); -// assert!(extra.contains_key("@git/status")); -// let git_status = extra.get("@git/status").unwrap(); -// assert!(git_status.is_object()); -// assert_eq!(git_status["git_user"], "$GIT_USER"); - -// Ok(()) -// } - -// #[test_log::test] -// fn test_agent_empty() -> crate::Result<()> { -// let kdl_agents = r#" -// agent "test" template=#true { -// } -// "#; - -// let config: GeneratorConfig = match parse("example.kdl", kdl_agents) { -// Ok(c) => c, -// Err(e) => { -// eprintln!("{:?}", miette::Report::new(e)); -// return Err(eyre!("failed to parse {kdl_agents}")); -// } -// }; -// assert!(!format!("{config:?}").is_empty()); -// assert_eq!(config.agents.len(), 1); -// let agent = config.agents[0].clone(); -// assert_eq!(agent.name, "test"); -// assert!(agent.model.is_none()); -// assert!(agent.is_template()); - -// Ok(()) -// } - -// #[test_log::test] -// fn test_agent_file_source() -> crate::Result<()> { -// let kdl_agent_file_source = r#" -// description "agent from file" -// prompt "Generate a test prompt" -// resource "file://resource.md" -// resource "file://README.md" -// include-mcp-json true -// tools "*" - -// allowed-tools "@awsdocs" -// hook { -// agent-spawn "spawn" { -// command "echo i have spawned" -// timeout-ms 1000 -// max-output-size 9000 -// cache-ttl-seconds 2 -// } -// user-prompt-submit "submit" { -// command "echo user submitted" -// } -// pre-tool-use "pre" { -// command "echo before tool" -// matcher "git.*" -// } -// post-tool-use "post" { -// command "echo after tool" -// } -// stop "stop" { -// command "echo stopped" -// } -// } - -// mcp "awsdocs" { -// command "aws-docs" -// args "--verbose" "--config=/path" -// env "RUST_LOG" "debug" -// env "PATH" "/usr/bin" -// header "Authorization" "Bearer token" -// timeout 5000 -// oauth { -// redirect-uri "127.0.0.1:7778" -// } -// } - -// alias "execute_bash" "shell" - -// native-tool { -// write { -// allow "./src/*" "./scripts/**" -// deny "Cargo.lock" -// override "/tmp" -// override "/var/log" -// } -// shell deny-by-default=#true { -// allow "git status .*" -// deny "git push .*" -// override "git pull .*" -// } -// } -// "#; - -// let agent: KdlAgentFileSource = match parse("example.kdl", kdl_agent_file_source) { -// Ok(c) => c, -// Err(e) => { -// eprintln!("{:?}", miette::Report::new(e)); -// return Err(eyre!("failed to parse {kdl_agent_file_source}")); -// } -// }; - -// assert_eq!(agent.description.unwrap_or_default(), "agent from file"); -// Ok(()) -// } - -// #[test_log::test] -// fn test_tool_setting_invalid_json() -> crate::Result<()> { -// let kdl = r#" -// agent "test" { -// tool-setting "bad" { -// json "{ invalid json }" -// } -// } -// "#; -// let config: GeneratorConfig = parse("test.kdl", kdl)?; -// let result = config.agents[0].extra_tool_settings(); -// assert!(result.is_err()); -// assert!( -// result -// .unwrap_err() -// .to_string() -// .contains("Failed to parse JSON") -// ); -// Ok(()) -// } -// } From adde70ddde49dce0ff4a10db378eb2c8435261a1 Mon Sep 17 00:00:00 2001 From: dougeEfresh Date: Sun, 4 Jan 2026 18:35:02 +0000 Subject: [PATCH 4/8] kdl master branch --- .kiro/generators/aws-test.kdl | 7 +- .kiro/generators/base.kdl | 78 ++++--- .kiro/generators/dependabot.kdl | 12 +- .kiro/generators/kg.kdl | 2 +- .kiro/global/aws-test.kdl | 3 +- .kiro/global/kg.kdl | 2 +- Cargo.lock | 343 +++++++++++++++++++------------ Cargo.toml | 36 ++-- src/agent/mod.rs | 24 ++- src/commands.rs | 3 +- src/config.rs | 101 ++++++++- src/config/agent.rs | 27 +-- src/config/agent_file.rs | 14 +- src/config/hook.rs | 44 ++-- src/config/mcp.rs | 165 +++++++-------- src/config/native.rs | 30 ++- src/generator/config_location.rs | 2 +- src/generator/discover.rs | 19 +- src/generator/merge.rs | 11 +- src/generator/mod.rs | 13 +- src/main.rs | 19 +- src/output.rs | 8 +- 22 files changed, 585 insertions(+), 378 deletions(-) diff --git a/.kiro/generators/aws-test.kdl b/.kiro/generators/aws-test.kdl index 3fe8b73..c675e4b 100644 --- a/.kiro/generators/aws-test.kdl +++ b/.kiro/generators/aws-test.kdl @@ -16,14 +16,17 @@ hook { native-tool { aws { - allow "ec2" "s3" + allow "ec2" + allow "s3" deny "iam" } } mcp "awsbilling" { command "uvx" - args "awslabs.billing-cost-management-mcp-server@latest" + args """ + awslabs.billing-cost-management-mcp-server@latest + """ env "FASTMCP_LOG_LEVEL" "ERROR" } diff --git a/.kiro/generators/base.kdl b/.kiro/generators/base.kdl index 0aa0b7c..8394619 100644 --- a/.kiro/generators/base.kdl +++ b/.kiro/generators/base.kdl @@ -1,39 +1,63 @@ description "Default agent for Kiro" -tools "*" -allowed-tools "read" "knowledge" "@fetch" +tools * +allowed-tools "read" +allowed-tools "knowledge" +allowed-tools "fetch" resource "file://README.md" resource "file://.amazonq/rules/**/*.md" -alias "execute_shell" "bash" + +alias "execute_shell" bash hook { - agent-spawn "echo" { - command "echo My name is Bob" - timeout-ms 4000 - cache-ttl-seconds 300 - } + agent-spawn echo { + command "echo My name is Bob" + timeout-ms 4000 + cache-ttl-seconds 300 + } } native-tool { - read { deny ".*Cargo.toml.*" ".*yarn.lock.*"; } - write { deny ".*Cargo.toml.*"; } - shell { - allow "git status" "git fetch" "git diff .*" \ - "git pull .*" "yarn .*" "pulumi preview .*" \ - "kubectl .*" "pdf2.*" "ps .*" "timeout.*" "pgrep.*" - - deny "git commit .*" "git push .*" "kubectl .*delete*" \ - ".*delete.*" "pulumi up.*" "^rm .*" \ - ".*destroy.*" ".*rollout.*" ".*kill.*" - } + read { + deny .*Cargo.toml.* + deny .*yarn.lock.* + } + write { + deny .*Cargo.toml.* + } + shell { + allow """ + git status + git fetch + git diff .* + git pull.* + yarn.* + pulumi preview .* + kubectl .* + pdf2.* + ps .* + timeout.* + pgrep.* + """ + deny """ + git commit .* + git push .* + kubectl .*delete* + .*delete.* + pulumi up.* + ^rm .* + .*destroy.* + .*rollout.* + .*kill.* + """ + } } - -mcp "rustdocs" { - command "mcp-docsrs" +mcp rustdocs { + command mcp-docsrs timeout 1200 } - -mcp "cargo" { - command "cargo-mcp" - args "--debug" - timeout 120000 +mcp cargo { + command cargo-mcp + args "--debug" + timeout 120000 } + diff --git a/.kiro/generators/dependabot.kdl b/.kiro/generators/dependabot.kdl index 0b3c77d..ed7f43d 100644 --- a/.kiro/generators/dependabot.kdl +++ b/.kiro/generators/dependabot.kdl @@ -1,8 +1,8 @@ description "I make life painful for developers" -native-tool { - shell { override "git commit .*"; override "git push .*"; } - read { override ".*Cargo.toml.*"; } - write { override ".*Cargo.toml.*"; } - aws disable-auto-readonly=true -} +// native-tool { +// shell { override "git commit .*"; override "git push .*"; } +// read { override ".*Cargo.toml.*"; } +// write { override ".*Cargo.toml.*"; } +// // aws disable-auto-readonly=true +//} diff --git a/.kiro/generators/kg.kdl b/.kiro/generators/kg.kdl index f245153..bbaa55c 100644 --- a/.kiro/generators/kg.kdl +++ b/.kiro/generators/kg.kdl @@ -1,4 +1,4 @@ -agent "base" template=true {} +agent "base" template=#true {} agent "aws-test" { inherits "base" hook { diff --git a/.kiro/global/aws-test.kdl b/.kiro/global/aws-test.kdl index 6931cb9..59202ec 100644 --- a/.kiro/global/aws-test.kdl +++ b/.kiro/global/aws-test.kdl @@ -4,7 +4,8 @@ resource "file://AGENTS.md" resource "file://README.md" native-tool { aws { - allow "ec2" "s3" + allow "ec2" + allow "s3" } } mcp "awsdocs" { diff --git a/.kiro/global/kg.kdl b/.kiro/global/kg.kdl index d3a668f..56975c7 100644 --- a/.kiro/global/kg.kdl +++ b/.kiro/global/kg.kdl @@ -1,3 +1,3 @@ -agent "base" template=true {} +agent "base" template=#true {} agent "aws-test" { inherits "base"; } agent "dependabot" { } diff --git a/Cargo.lock b/Cargo.lock index d552e9e..3244b63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,6 +115,83 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "arborium" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb0f3ba8f3bc4322ff8c97a0a2090a4bebf358406ea375c7a17d8954f2362a4" +dependencies = [ + "arborium-highlight", + "arborium-json", + "arborium-rust", + "arborium-theme", + "arborium-tree-sitter", + "dlmalloc", +] + +[[package]] +name = "arborium-highlight" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975a3508e6b26b013345c7c203ee36c81d03f9d30ae4e55dec4b6079c205c6db" +dependencies = [ + "arborium-theme", + "arborium-tree-sitter", + "streaming-iterator", +] + +[[package]] +name = "arborium-json" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9496b97a3b4a83548e81ca1e30a26d14ba1239091d842c72729df2a0d6cb774c" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-rust" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18ff2b7196273ba33fc35daf2c409f65bf3df7c3a25b11cbd328ab39ee57c1e" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-sysroot" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d83ed18d08e513f5d80bd0f7ed2b3a2369ddbb49d51d674025d94be8c2b9ed8" +dependencies = [ + "cc", + "dlmalloc", +] + +[[package]] +name = "arborium-theme" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dbced2fec10201488d18dbac859f96d479dc089e3caa7f9f2710d199c08604" + +[[package]] +name = "arborium-tree-sitter" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f086fd94acc289a46b431a8c2a9eba075000e94837e17c647dae43551225d40f" +dependencies = [ + "arborium-sysroot", + "cc", + "regex", + "regex-syntax", + "streaming-iterator", + "tree-sitter-language", +] + [[package]] name = "arraydeque" version = "0.5.1" @@ -209,9 +286,9 @@ checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytecount" @@ -227,9 +304,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "shlex", @@ -249,9 +326,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -259,9 +336,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -289,34 +366,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" -[[package]] -name = "color-eyre" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" -dependencies = [ - "backtrace", - "color-spantrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", - "tracing-error", - "url", -] - -[[package]] -name = "color-spantrace" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" -dependencies = [ - "once_cell", - "owo-colors", - "tracing-core", - "tracing-error", -] - [[package]] name = "colorchoice" version = "1.0.4" @@ -334,9 +383,9 @@ dependencies = [ [[package]] name = "console" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" dependencies = [ "encode_unicode", "libc", @@ -422,6 +471,17 @@ dependencies = [ "syn", ] +[[package]] +name = "dlmalloc" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6738d2e996274e499bc7b0d693c858b7720b9cd2543a0643a3087e6cb0a4fa16" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "document-features" version = "0.2.12" @@ -497,59 +557,57 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - [[package]] name = "facet" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ee49c69f8a398d01d9b160e3e6288c1a5f7d756e8377f0530bbb4019aa1616" +version = "0.41.0" +source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" dependencies = [ "autocfg", "facet-core", "facet-macros", - "static_assertions", + "facet-reflect", ] [[package]] name = "facet-core" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ca85b6f8c289d86e5a0daa6b402ed1edf4001ad9b6ead357cc047fff680e0d" +version = "0.41.0" +source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" dependencies = [ "autocfg", "impls", ] [[package]] -name = "facet-kdl" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b52d9f35c93a85109d9d1d9042fbc1b02972c28eaceb36374995e9d166f4019" +name = "facet-format" +version = "0.41.0" +source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" dependencies = [ - "facet", "facet-core", + "facet-path", "facet-reflect", "facet-singularize", "facet-solver", + "miette", +] + +[[package]] +name = "facet-kdl" +version = "0.41.0" +source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +dependencies = [ + "facet", + "facet-core", + "facet-format", + "facet-path", + "facet-reflect", "kdl", - "log", "miette", ] [[package]] name = "facet-macro-parse" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "294183c810413075f9c3f075c0b3554d04ad06207dad18debc649a48779321f6" +version = "0.41.0" +source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" dependencies = [ "facet-macro-types", "proc-macro2", @@ -558,9 +616,8 @@ dependencies = [ [[package]] name = "facet-macro-types" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8335dd3290eb5780aa40fb5b0da6c1a1c08980af6ada54c2e0d8cbbcd52b8f33" +version = "0.41.0" +source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" dependencies = [ "proc-macro2", "quote", @@ -569,18 +626,16 @@ dependencies = [ [[package]] name = "facet-macros" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54f7c8e20f24f6c933290da20e76ce8b62a28ea7f16ea173a1aa21cb2ebf61f0" +version = "0.41.0" +source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" dependencies = [ "facet-macros-impl", ] [[package]] name = "facet-macros-impl" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc36ba0859bb5fc539e9fb9ed4dab7a5af3b9dbf080e92adaeb5041c58971fcb" +version = "0.41.0" +source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" dependencies = [ "facet-macro-parse", "facet-macro-types", @@ -590,11 +645,32 @@ dependencies = [ "unsynn", ] +[[package]] +name = "facet-path" +version = "0.41.0" +source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +dependencies = [ + "arborium", + "facet-core", + "facet-pretty", + "miette", + "miette-arborium", +] + +[[package]] +name = "facet-pretty" +version = "0.41.0" +source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +dependencies = [ + "facet-core", + "facet-reflect", + "owo-colors", +] + [[package]] name = "facet-reflect" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ab47f7ccaed7b782b4cdbfa3482f16720c0e7e31c38bf5f7da8b8f8c988690" +version = "0.41.0" +source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" dependencies = [ "facet-core", "miette", @@ -602,19 +678,16 @@ dependencies = [ [[package]] name = "facet-singularize" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd14cadea48902b862d1f9256f1eac9102680d3fc105e5888008e219f2de6023" +version = "0.41.0" +source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" [[package]] name = "facet-solver" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a23bcde5d4f562dfed81ca31c15c241536b1b6ef0a6e46cc17d8963b9f9f33" +version = "0.41.0" +source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" dependencies = [ "facet-core", "facet-reflect", - "strsim", ] [[package]] @@ -636,9 +709,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "fluent-uri" @@ -1099,12 +1172,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a46645bbd70538861a90d0f26c31537cdf1e44aae99a794fb75a664b70951bc" -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - [[package]] name = "indexmap" version = "2.12.1" @@ -1132,9 +1199,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -1154,9 +1221,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" @@ -1214,7 +1281,6 @@ name = "kiro-generator" version = "0.1.1-rc.6" dependencies = [ "clap", - "color-eyre", "colored", "dirs", "emojis-rs", @@ -1246,15 +1312,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags", "libc", @@ -1327,6 +1393,19 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "miette-arborium" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e011023c9185a2e63af9ba02d09736a0935596225f710f3e279101154c507" +dependencies = [ + "arborium", + "arborium-highlight", + "arborium-theme", + "miette", + "owo-colors", +] + [[package]] name = "miette-derive" version = "7.6.0" @@ -1644,9 +1723,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] @@ -1754,9 +1833,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.25" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", @@ -1822,9 +1901,9 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", @@ -1848,9 +1927,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" dependencies = [ "zeroize", ] @@ -1874,9 +1953,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "schannel" @@ -1948,15 +2027,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -2026,10 +2105,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "static_assertions" -version = "1.1.0" +name = "streaming-iterator" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" [[package]] name = "strsim" @@ -2079,9 +2158,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" dependencies = [ "proc-macro2", "quote", @@ -2131,9 +2210,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -2244,9 +2323,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -2348,9 +2427,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2370,9 +2449,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -2417,6 +2496,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tree-sitter-language" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae62f7eae5eb549c71b76658648b72cc6111f2d87d24a1e31fa907f4943e3ce" + [[package]] name = "try-lock" version = "0.2.5" @@ -2989,3 +3074,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317f17ff091ac4515f17cc7a190d2769a8c9a96d227de5d64b500b01cda8f2cd" diff --git a/Cargo.toml b/Cargo.toml index 703c544..06cb549 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,22 +3,21 @@ name = "kiro-generator" version = "0.1.1-rc.6" edition = "2024" description = "Kiro Agent CLI configuration management" -repository = "https://github.com/CarteraMesh/q-generator" -license = "MIT" -authors = ["gh@cartera-mesh.com"] documentation = "https://docs.rs/q-generator" +readme = "README.md" homepage = "https://github.com/CarteraMesh/q-generator" -keywords = ["kiro", "cli", "agents", "AI"] +repository = "https://github.com/CarteraMesh/q-generator" +license = "MIT" +keywords = ["AI", "agents", "cli", "kiro"] categories = ["command-line-interface", "config"] -readme = "README.md" [package.metadata.deb] maintainer = "Dougefresh " section = "utility" priority = "optional" assets = [ - ["target/release/kg", "usr/bin/", "755"], - { source = "README.md", dest = "usr/share/doc/kiro-generator/README", mode = "644"}, + ["target/release/kg", "usr/bin/", "755"], + { source = "README.md", dest = "usr/share/doc/kiro-generator/README", mode = "644" }, ] [[bin]] @@ -27,17 +26,22 @@ path = "src/main.rs" [dependencies] clap = { version = "4", features = ["cargo", "derive", "env", "unicode"] } -color-eyre = { version = "0.6", features = ["issue-url"] } colored = "3.0.0" dirs = "6" -# emojis = "0.8.0" emojis-rs = "0.1.3" enum-iterator = "2.3.0" -facet = "0.34.0" -facet-kdl = "0.34.0" +facet = { git = "https://github.com/facet-rs/facet.git", features = [ + "auto-traits", + "reflect", + "simd" +] } +facet-kdl = { git = "https://github.com/facet-rs/facet.git" } futures = "0.3" indoc = "2.0.7" -jsonschema = { version = "0.37", default-features = false, features = ["resolve-async", "resolve-file"] } +jsonschema = { version = "0.37", default-features = false, features = [ + "resolve-async", + "resolve-file" +] } miette = { version = "7", features = ["fancy"] } serde = { version = "1", features = ["derive"] } serde_json = { version = "1" } @@ -54,7 +58,7 @@ nix = { version = "0.30.1", features = ["user"] } test-log = { version = "0.2", default-features = false, features = ["trace"] } [profile.release] -strip = false # Keep symbols for better backtraces -debug = 1 # Line numbers only (smaller than full debug=2) -lto = "thin" # Faster builds than "fat", still good optimization -codegen-units = 16 # Default, balances compile time vs optimization +debug = 1 # Line numbers only (smaller than full debug=2) +strip = false # Keep symbols for better backtraces +lto = "thin" # Faster builds than "fat", still good optimization +codegen-units = 16 # Default, balances compile time vs optimization diff --git a/src/agent/mod.rs b/src/agent/mod.rs index fd15c4c..c91c3fc 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -8,7 +8,7 @@ pub const DEFAULT_APPROVE: [&str; 0] = []; use { super::agent::hook::{Hook, HookTrigger}, crate::{Result, config::KdlAgent}, - color_eyre::eyre::eyre, + miette::IntoDiagnostic, serde::{Deserialize, Serialize}, std::{ collections::{HashMap, HashSet}, @@ -78,15 +78,17 @@ impl Display for Agent { impl Agent { pub fn validate(&self) -> Result<()> { - let schema: serde_json::Value = serde_json::from_str(crate::schema::SCHEMA)?; - let validator = jsonschema::validator_for(&schema)?; - let instance = serde_json::to_value(self)?; + // TODO cache this + let schema: serde_json::Value = + serde_json::from_str(crate::schema::SCHEMA).into_diagnostic()?; + let validator = jsonschema::validator_for(&schema).into_diagnostic()?; + let instance = serde_json::to_value(self).into_diagnostic()?; if let Err(e) = validator.validate(&instance) { - return Err(eyre!( + return Err(crate::format_err!( "Validation error: {}\n{}", e, - serde_json::to_string(&instance)? + serde_json::to_string(&instance).unwrap_or_default() )); } Ok(()) @@ -94,7 +96,7 @@ impl Agent { } impl TryFrom<&KdlAgent> for Agent { - type Error = color_eyre::Report; + type Error = miette::Report; fn try_from(value: &KdlAgent) -> std::result::Result { let native_tools = &value.native_tool; @@ -106,7 +108,7 @@ impl TryFrom<&KdlAgent> for Agent { tools_settings.insert( tool_name.to_string(), serde_json::to_value(&tool).map_err(|e| { - eyre!( + crate::format_err!( "Failed to serialize {tool_name} tool configuration {e}" ) @@ -119,7 +121,7 @@ impl TryFrom<&KdlAgent> for Agent { tools_settings.insert( tool_name.to_string(), serde_json::to_value(&tool).map_err(|e| { - eyre!( + crate::format_err!( "Failed to serialize {tool_name} tool configuration {e}" ) @@ -132,7 +134,7 @@ impl TryFrom<&KdlAgent> for Agent { tools_settings.insert( tool_name.to_string(), serde_json::to_value(&tool).map_err(|e| { - eyre!( + crate::format_err!( "Failed to serialize {tool_name} tool configuration {e}" ) @@ -145,7 +147,7 @@ impl TryFrom<&KdlAgent> for Agent { tools_settings.insert( tool_name.to_string(), serde_json::to_value(&tool).map_err(|e| { - eyre!( + crate::format_err!( "Failed to serialize {tool_name} tool configuration {e}" ) diff --git a/src/commands.rs b/src/commands.rs index 0788c12..06f9e1b 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -5,7 +5,6 @@ use { Subcommand, builder::{Styles, styling::AnsiColor}, }, - color_eyre::eyre::eyre, std::{io::IsTerminal, path::PathBuf}, }; @@ -121,7 +120,7 @@ impl Cli { /// Return home dir and ~/.kiro/generators/kg.kdl pub fn config(&self) -> crate::Result<(PathBuf, PathBuf)> { - let home_dir = dirs::home_dir().ok_or(eyre!("cannot locate home directory"))?; + let home_dir = dirs::home_dir().ok_or(crate::format_err!("unable to find HOME dir"))?; let cfg = home_dir.join(".kiro").join("generators"); Ok((home_dir, cfg)) } diff --git a/src/config.rs b/src/config.rs index 237bbb6..9419fd3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,7 +10,7 @@ use { crate::Fs, facet::Facet, facet_kdl as kdl, - miette::IntoDiagnostic, + miette::{GraphicalReportHandler, GraphicalTheme, IntoDiagnostic}, std::{ collections::{HashMap, HashSet}, fmt::{Debug, Display}, @@ -19,6 +19,7 @@ use { }; pub(crate) type ConfigResult = miette::Result; + #[derive(Facet, Debug, Default, PartialEq, Clone, Eq, Hash)] #[facet(default)] pub(super) struct GenericItem { @@ -38,6 +39,65 @@ impl AsRef for GenericItem { } } +#[derive(Facet, Debug, Default, PartialEq, Clone, Eq)] +#[facet(default)] +pub(super) struct GenericItemList { + #[facet(kdl::arguments)] + pub item: HashSet, +} + +impl Display for GenericItemList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.item) + } +} + +impl AsRef> for GenericItemList { + fn as_ref(&self) -> &HashSet { + &self.item + } +} + +impl GenericItemList { + fn len(&self) -> usize { + self.item.len() + } +} + +#[derive(Facet, Debug, Default, PartialEq, Clone, Eq)] +#[facet(default)] +pub(super) struct GenericVec { + #[facet(kdl::arguments)] + pub item: Vec, +} + +impl Display for GenericVec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.item) + } +} + +impl AsRef> for GenericVec { + fn as_ref(&self) -> &Vec { + &self.item + } +} + +impl From for HashMap { + fn from(list: GenericVec) -> HashMap { + list.item + .chunks_exact(2) + .map(|chunk| (chunk[0].clone(), chunk[1].clone())) + .collect() + } +} + +impl GenericVec { + fn len(&self) -> usize { + self.item.len() + } +} + #[derive(Facet, Copy, Default, Clone, Debug, PartialEq, Eq)] #[facet(default)] pub(super) struct IntDoc { @@ -50,6 +110,15 @@ impl AsRef for IntDoc { } } +fn print_error(name: &str, e: &facet_kdl::KdlDeserializeError) { + // let d = e.into_diagnostics(); + eprintln!("\n=== Miette render ==="); + let mut output = String::new(); + let handler = GraphicalReportHandler::new_themed(GraphicalTheme::unicode()); + handler.render_report(&mut output, e).unwrap(); + eprintln!("{}", output); +} + pub(super) fn split_newline(list: Vec) -> HashSet { list.iter() .flat_map(|f| f.item.split('\n')) @@ -59,22 +128,32 @@ pub(super) fn split_newline(list: Vec) -> HashSet { .collect() } -pub fn kdl_parse_path<'facet: 'shape, 'shape, T>( - fs: &Fs, - content: impl AsRef, -) -> ConfigResult +pub fn kdl_parse_path(fs: &Fs, path: impl AsRef) -> Option> where - T: facet::Facet<'facet>, + T: for<'a> facet::Facet<'a>, { - Ok(kdl_parse("")?) + if fs.exists(&path) { + match fs.read_to_string_sync(&path).into_diagnostic() { + Err(e) => Some(Err(e)), + Ok(content) => Some(kdl::from_str(&content).into_diagnostic()), + } + } else { + None + } } -pub fn kdl_parse<'input, 'facet: 'shape, 'shape, T>(content: &'input str) -> ConfigResult +#[cfg(test)] +pub(crate) fn kdl_parse(content: &str) -> ConfigResult where - T: facet::Facet<'facet>, - 'input: 'facet, + T: for<'a> facet::Facet<'a>, { - kdl::from_str(content).into_diagnostic() + match kdl::from_str::(content) { + Err(e) => { + print_error("test", &e); + Err(crate::format_err!("{e}")) + } + Ok(r) => Ok(r), + } } #[derive(facet::Facet, Default)] diff --git a/src/config/agent.rs b/src/config/agent.rs index ae2de43..d192ea7 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -9,7 +9,6 @@ use { agent::{CustomToolConfig, OriginalToolName}, config::split_newline, }, - color_eyre::eyre::WrapErr, facet::Facet, facet_kdl as kdl, std::{ @@ -45,18 +44,20 @@ struct Json { impl ToolSetting { #[allow(dead_code)] fn to_value(&self) -> crate::Result<(String, serde_json::Value)> { - let v: serde_json::Value = serde_json::from_str(&self.json.value) - .wrap_err_with(|| format!("Failed to parse JSON for tool-setting '{}'", self.name))?; - - if !v.is_object() { - return Err(color_eyre::eyre::eyre!( - "tool-setting '{}' must be a JSON object, got: {}", - self.name, - v - )); - } - - Ok((self.name.clone(), v)) + todo!() + // let v: serde_json::Value = serde_json::from_str(&self.json.value) + // .wrap_err_with(|| format!("Failed to parse JSON for tool-setting + // '{}'", self.name))?; + // + // if !v.is_object() { + // return Err(crate::format_err!( + // "tool-setting '{}' must be a JSON object, got: {}", + // self.name, + // v + // )); + // } + // + // Ok((self.name.clone(), v)) } } diff --git a/src/config/agent_file.rs b/src/config/agent_file.rs index 41a27da..2b7d0c9 100644 --- a/src/config/agent_file.rs +++ b/src/config/agent_file.rs @@ -7,10 +7,9 @@ use { native::NativeToolsDoc, }, crate::Fs, - color_eyre::eyre::eyre, facet::Facet, facet_kdl as kdl, - miette::IntoDiagnostic, + miette::{Context, IntoDiagnostic}, std::path::Path, }; @@ -21,7 +20,7 @@ pub(super) struct BoolDoc { pub value: bool, } #[derive(Facet, Clone, Default)] -#[facet(rename_all = "kebab-case", default)] +#[facet(deny_unknown_fields, rename_all = "kebab-case", default)] pub struct KdlAgentFileDoc { #[facet(kdl::child, default)] pub(super) description: Option, @@ -66,10 +65,13 @@ impl KdlAgentDoc { return Ok(None); } - let content = fs.read_to_string_sync(&path)?; - let agent: KdlAgentFileDoc = kdl::from_str(&content) + let content = fs + .read_to_string_sync(&path) .into_diagnostic() - .map_err(|e| eyre!("failed {} {e}", path.as_ref().display()))?; + .wrap_err(format!("unable to read {}", path.as_ref().display()))?; + let agent: KdlAgentFileDoc = kdl::from_str(&content).into_diagnostic().map_err(|e| { + crate::format_err!("failed {} error:'{e}'\n{content}", path.as_ref().display()) + })?; Ok(Some(Self::from_file_source(name, agent))) } diff --git a/src/config/hook.rs b/src/config/hook.rs index 00991e3..5b2dfa0 100644 --- a/src/config/hook.rs +++ b/src/config/hook.rs @@ -1,5 +1,4 @@ use { - super::IntDoc, crate::agent::hook::{Hook, HookTrigger}, facet::Facet, facet_kdl as kdl, @@ -14,24 +13,24 @@ macro_rules! define_hook_doc { #[facet(kdl::argument)] pub name: String, #[facet(kdl::child, default)] - command: GenericValue, - #[facet(default, kdl::child)] - timeout_ms: IntDoc, + command: String, + #[facet(kdl::child, default, rename = "timeout-ms")] + timeout_ms: u64, + #[facet(kdl::child, default, rename = "max-output-size")] + max_output_size: u64, + #[facet(kdl::child, default, rename = "cache-ttl")] + cache_ttl_seconds: u64, #[facet(kdl::child, default)] - max_output_size: IntDoc, - #[facet(kdl::child, default)] - cache_ttl_seconds: IntDoc, - #[facet(kdl::child, default)] - matcher: Option, + matcher: Option, } impl From<$name> for Hook { fn from(value: $name) -> Hook { Hook { - command: value.command.value, - timeout_ms: value.timeout_ms.value, - max_output_size: value.max_output_size.value, - cache_ttl_seconds: value.cache_ttl_seconds.value, - matcher: value.matcher.map(|m| m.value), + command: value.command, + timeout_ms: value.timeout_ms, + max_output_size: value.max_output_size, + cache_ttl_seconds: value.cache_ttl_seconds, + matcher: value.matcher, } } } @@ -52,15 +51,14 @@ define_hook_doc!(HookPostToolUseDoc); define_hook_doc!(HookStopDoc); #[derive(Facet, Clone, Default, Debug, PartialEq, Eq)] -#[facet(default, rename_all = "kebab-case")] pub struct HookDoc { - #[facet(kdl::children, default)] + #[facet(kdl::children, default, rename = "agent-spawn")] pub agent_spawn: Vec, - #[facet(kdl::children, default)] + #[facet(kdl::children, default, rename = "user-prompt-submit")] pub user_prompt_submit: Vec, - #[facet(kdl::children, default)] + #[facet(kdl::children, default, rename = "pre-tool-use")] pub pre_tool_use: Vec, - #[facet(kdl::children, default)] + #[facet(kdl::children, default, rename = "post-tool-use")] pub post_tool_use: Vec, #[facet(kdl::children, default)] pub stop: Vec, @@ -149,7 +147,11 @@ fn merge_hooks( #[cfg(test)] mod tests { - use {super::*, crate::Result, std::time::Duration}; + use { + super::*, + crate::{Result, config::kdl_parse}, + std::time::Duration, + }; fn rando() -> HashMap { let value = std::time::SystemTime::now() @@ -206,7 +208,7 @@ mod tests { cache-ttl-seconds 0 } "#; - let doc: HookDoc = facet_kdl::from_str(kdl)?; + let doc: HookDoc = kdl_parse(kdl)?; let doc = HookPart::from(doc); assert_eq!(1, doc.agent_spawn.len()); diff --git a/src/config/mcp.rs b/src/config/mcp.rs index c79a41b..c0f38fd 100644 --- a/src/config/mcp.rs +++ b/src/config/mcp.rs @@ -1,38 +1,21 @@ use { - super::IntDoc, - crate::{agent::CustomToolConfig, config::GenericItem}, + crate::{ + agent::CustomToolConfig, + config::{GenericItemList, GenericVec}, + }, facet::Facet, facet_kdl as kdl, + std::collections::HashMap, }; #[derive(Facet, Clone, Debug)] -struct EnvVar { +struct KeyVal { #[facet(kdl::argument)] key: String, #[facet(kdl::argument)] value: String, } -#[derive(Facet, Clone, Debug)] -struct Header { - #[facet(kdl::argument)] - key: String, - #[facet(kdl::argument)] - value: String, -} - -#[derive(Facet, Clone, Debug, Eq, PartialEq)] -pub struct RedirectUri { - #[facet(kdl::argument)] - pub value: String, -} - -#[derive(Facet, Clone, Debug)] -pub struct Disabled { - #[facet(kdl::argument)] - pub value: bool, -} - #[derive(Facet, Default, Clone, Debug)] #[facet(rename_all = "kebab-case", default)] pub struct CustomToolConfigDoc { @@ -40,56 +23,41 @@ pub struct CustomToolConfigDoc { pub name: String, #[facet(kdl::child, default)] - pub url: Option, + pub url: String, #[facet(kdl::child, default)] - pub command: Option, - - #[facet(kdl::children, default)] - args: Vec, - - #[facet(kdl::children, default)] - env: Vec, + pub command: String, - #[facet(kdl::children, default)] - header: Vec
, + #[facet(kdl::child, default)] + args: GenericItemList, #[facet(kdl::child, default)] - pub(super) timeout: IntDoc, + env: GenericVec, #[facet(kdl::child, default)] - pub disabled: Option, -} + header: GenericVec, -#[derive(Facet, Clone, Debug)] -pub struct Url { - #[facet(kdl::argument)] - pub value: String, -} + #[facet(kdl::child, default)] + pub(super) timeout: u64, -#[derive(Facet, Clone, Debug)] -pub struct Command { - #[facet(kdl::argument)] - pub value: String, + #[facet(kdl::property, default)] + pub disabled: bool, } impl From for CustomToolConfig { fn from(value: CustomToolConfigDoc) -> Self { - let command = value.command.map(|c| c.value).unwrap_or_default(); - let url = value.url.map(|u| u.value).unwrap_or_default(); - Self { - url, - command, - args: value.args.into_iter().map(|v| v.item).collect(), - timeout: if value.timeout.value == 0 { + url: value.url, + command: value.command, + args: value.args.item.into_iter().collect(), + timeout: if value.timeout == 0 { crate::agent::tool_default_timeout() } else { - value.timeout.value + value.timeout }, - disabled: value.disabled.map(|d| d.value).unwrap_or(false), - headers: value.header.into_iter().map(|h| (h.key, h.value)).collect(), - env: value.env.into_iter().map(|e| (e.key, e.value)).collect(), + disabled: value.disabled, + headers: value.header.into(), + env: value.env.into(), } } } @@ -102,7 +70,11 @@ impl From<&CustomToolConfigDoc> for CustomToolConfig { #[cfg(test)] mod tests { - use super::*; + use { + super::*, + crate::config::{ConfigResult, kdl_parse}, + indoc::indoc, + }; #[derive(Facet, Debug)] struct McpDoc { @@ -111,31 +83,35 @@ mod tests { } #[test] - fn parse_basic_mcp() { - let kdl = r#"mcp "rustdocs" { - command "rust-docs-mcp" - timeout 1000 - }"#; - - let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); + fn parse_basic_mcp() -> ConfigResult<()> { + let kdl = indoc! { + r#"mcp "rustdocs" { + command "rust-docs-mcp" + timeout 1000 + }"# + }; + + let doc: McpDoc = kdl_parse(kdl)?; assert_eq!(doc.mcp.name, "rustdocs"); - assert_eq!(doc.mcp.command.unwrap().value, "rust-docs-mcp"); - assert_eq!(doc.mcp.timeout.value, 1000); + assert_eq!(doc.mcp.command, "rust-docs-mcp"); + assert_eq!(doc.mcp.timeout, 1000); + Ok(()) } #[test] - fn parse_mcp_with_url() { + fn parse_mcp_with_url() -> ConfigResult<()> { let kdl = r#"mcp "remote" { url "http://localhost:8080" }"#; - let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); + let doc: McpDoc = facet_kdl::from_str(kdl)?; assert_eq!(doc.mcp.name, "remote"); - assert_eq!(doc.mcp.url.unwrap().value, "http://localhost:8080"); + assert_eq!(doc.mcp.url, "http://localhost:8080"); + Ok(()) } #[test] - fn parse_mcp_with_env_and_headers() { + fn parse_mcp_with_env_and_headers() -> ConfigResult<()> { let kdl = r#"mcp "api" { command "api-server" env "API_KEY" "secret123" @@ -143,50 +119,63 @@ mod tests { header "Authorization" "Bearer token" header "Content-Type" "application/json" }"#; - - let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); - assert_eq!(doc.mcp.env.len(), 2); - assert_eq!(doc.mcp.header.len(), 2); + let doc: McpDoc = facet_kdl::from_str(kdl)?; + assert_eq!(doc.mcp.env.len(), 4); + assert_eq!(doc.mcp.header.len(), 4); + + let env: HashMap = doc.mcp.env.into(); + assert_eq!(env.len(), 2); + assert_eq!(env.get("API_KEY"), Some(&"secret123".to_string())); + assert_eq!(env.get("DEBUG"), Some(&"true".to_string())); + + let header: HashMap = doc.mcp.header.into(); + assert_eq!(header.len(), 2); + assert_eq!(header.get("Authorization"), Some(&"Bearer token".to_string())); + assert_eq!(header.get("Content-Type"), Some(&"application/json".to_string())); + Ok(()) } #[test] - fn parse_mcp_with_args() { - let kdl = r#"mcp "tool" { + fn parse_mcp_with_args() -> ConfigResult<()> { + let kdl = indoc! { r#"mcp "tool" { command "my-tool" - args "--verbose" "--output" "json" - }"#; - - let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); - let args: Vec = doc.mcp.args.into_iter().map(|v| v.item).collect(); - assert_eq!(args, vec!["--verbose", "--output", "json"]); + args "--verbose" "--output=json" + }"# + }; + + let doc: McpDoc = kdl_parse(kdl)?; + let args: Vec = doc.mcp.args.item.into_iter().collect(); + assert_eq!(args, vec!["--verbose", "--output=json"]); + Ok(()) } #[test] - fn convert_to_custom_tool_config() { - let kdl = r#"mcp "test" { + fn convert_to_custom_tool_config() -> ConfigResult<()> { + let kdl = r#"mcp "test" disabled=#true { command "test-cmd" timeout 5000 - disabled #true }"#; - let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); + let doc: McpDoc = facet_kdl::from_str(kdl)?; let config: CustomToolConfig = doc.mcp.into(); assert_eq!(config.command, "test-cmd"); assert_eq!(config.timeout, 5000); assert!(config.disabled); + Ok(()) } #[test] - fn default_timeout_when_zero() { + fn default_timeout_when_zero() -> ConfigResult<()> { let kdl = r#"mcp "test" { command "test-cmd" timeout 0 }"#; - let doc: McpDoc = facet_kdl::from_str(kdl).unwrap(); + let doc: McpDoc = facet_kdl::from_str(kdl)?; let config: CustomToolConfig = doc.mcp.into(); assert_eq!(config.timeout, crate::agent::tool_default_timeout()); + Ok(()) } } diff --git a/src/config/native.rs b/src/config/native.rs index 5bdaf2e..593e255 100644 --- a/src/config/native.rs +++ b/src/config/native.rs @@ -1,5 +1,5 @@ use { - super::{GenericItem, split_newline}, + super::GenericItemList, crate::agent::{ AwsTool as KiroAwsTool, ExecuteShellTool as KiroShellTool, @@ -41,12 +41,12 @@ macro_rules! define_kdl_doc { #[derive(Facet, Clone, Debug, Default, PartialEq, Eq)] #[facet(default, rename_all = "kebab-case")] pub struct $name { - #[facet(default, kdl::children)] - pub(super) allows: Vec, - #[facet(default, kdl::children)] - pub(super) denies: Vec, - #[facet(default, kdl::children)] - pub(super) overrides: Vec, + #[facet(default, kdl::child)] + pub(super) allows: GenericItemList, + #[facet(default, kdl::child)] + pub(super) denies: GenericItemList, + #[facet(default, kdl::child)] + pub(super) overrides: GenericItemList, #[facet(default, kdl::property)] pub deny_by_default: Option, #[facet(default, kdl::property)] @@ -60,9 +60,9 @@ macro_rules! define_tool_into { impl From<$name> for $to { fn from(value: $name) -> $to { $to { - allows: split_newline(value.allows), - denies: split_newline(value.denies), - overrides: split_newline(value.overrides), + allows: value.allows.item, + denies: value.denies.item, + overrides: value.overrides.item, deny_by_default: value.deny_by_default, disable_auto_readonly: value.disable_auto_readonly, } @@ -85,7 +85,6 @@ define_tool_into!(WriteToolDoc, WriteTool); define_tool_into!(ReadToolDoc, ReadTool); #[derive(Facet, Default, Clone, Debug, PartialEq, Eq)] -#[facet(default, deny_unknown_fields)] pub struct NativeToolsDoc { #[facet(default, kdl::child)] pub shell: ExecuteShellToolDoc, @@ -253,12 +252,9 @@ mod tests { fn parse_shell_tool() -> ConfigResult<()> { let kdl = r#" shell deny-by-default=#true disable-auto-readonly=#false { - allow """ - ls .* - git status - """ - deny "rm -rf /" - override "git push" + allows "ls .*" "git status" + denies "rm -rf /" + overrides "git push" } "#; diff --git a/src/generator/config_location.rs b/src/generator/config_location.rs index 273a3dd..58e06bb 100644 --- a/src/generator/config_location.rs +++ b/src/generator/config_location.rs @@ -49,7 +49,7 @@ impl ConfigLocation { let local_exists = fs.exists(self.local_kg()); if !global_exists && !local_exists { - return Err(eyre!( + return Err(crate::format_err!( "no kg.kdl found at global ({}) or local ({})", self.global_kg().display(), self.local_kg().display() diff --git a/src/generator/discover.rs b/src/generator/discover.rs index 89e59a3..f4ee8b9 100644 --- a/src/generator/discover.rs +++ b/src/generator/discover.rs @@ -1,22 +1,17 @@ use { super::*, crate::config::{GeneratorConfig, GeneratorConfigDoc, KdlAgent, KdlAgentDoc}, - miette::IntoDiagnostic, std::{fmt::Display, ops::Deref, path::Path}, }; pub fn load_inline(fs: &Fs, path: impl AsRef) -> Result { - if fs.exists(&path) { - let content = fs - .read_to_string_sync(&path) - .wrap_err_with(|| format!("failed to read path '{}'", path.as_ref().display()))?; - - let doc: GeneratorConfigDoc = facet_kdl::from_str(&content) - .into_diagnostic() - .map_err(|e| eyre!("failed to parse from {} err:{e}", path.as_ref().display()))?; - Ok(doc.into()) - } else { - Ok(GeneratorConfig::default()) + let doc: Option> = crate::config::kdl_parse_path(fs, path); + match doc { + None => Ok(GeneratorConfig::default()), + Some(d) => { + let d = d?; + Ok(d.into()) + } } } diff --git a/src/generator/merge.rs b/src/generator/merge.rs index 5c6aa7a..526a383 100644 --- a/src/generator/merge.rs +++ b/src/generator/merge.rs @@ -10,7 +10,7 @@ impl Generator { visited: &mut HashSet, ) -> Result> { if visited.contains(&agent.name) { - return Err(color_eyre::eyre::eyre!( + return Err(crate::format_err!( "Circular inheritance detected: {} already in chain", agent.name )); @@ -23,7 +23,7 @@ impl Generator { .resolved .agents .get(parent_name) - .ok_or_else(|| color_eyre::eyre::eyre!("Agent '{parent_name}' not found"))?; + .ok_or_else(|| crate::format_err!("Agent '{parent_name}' not found"))?; let parent_chain = self.resolve_transitive_inheritance(parent, visited)?; for p in parent_chain { @@ -54,9 +54,10 @@ impl Generator { let mut merged = agent.clone(); for parent_name in parents.iter().rev() { - let parent = self.resolved.agents.get(parent_name).ok_or_else(|| { - color_eyre::eyre::eyre!("Parent agent '{parent_name}' not found") - })?; + let parent = + self.resolved.agents.get(parent_name).ok_or_else(|| { + crate::format_err!("Parent agent '{parent_name}' not found") + })?; merged = merged.merge(parent.clone()); } diff --git a/src/generator/mod.rs b/src/generator/mod.rs index 7378067..f7d7b0d 100644 --- a/src/generator/mod.rs +++ b/src/generator/mod.rs @@ -5,7 +5,7 @@ use { config::KdlAgent, os::Fs, }, - color_eyre::eyre::{Context, eyre}, + miette::{Context, IntoDiagnostic}, serde::Serialize, std::{ collections::{HashMap, HashSet}, @@ -155,7 +155,8 @@ impl Generator { self.fs .create_dir_all(&result.destination) .await - .with_context(|| { + .into_diagnostic() + .wrap_err_with(|| { format!( "failed to create directory {}", result.destination.display() @@ -168,9 +169,13 @@ impl Generator { .join(format!("{}.json", result.agent.name)); self.fs - .write(&out, serde_json::to_string_pretty(&result.kiro_agent)?) + .write( + &out, + serde_json::to_string_pretty(&result.kiro_agent).into_diagnostic()?, + ) .await - .with_context(|| format!("failed to write file {}", out.display()))?; + .into_diagnostic() + .wrap_err_with(|| format!("failed to write file {}", out.display()))?; } Ok(result) } diff --git a/src/main.rs b/src/main.rs index e58ec73..54ed3d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,16 +8,18 @@ pub mod output; mod schema; mod source; +pub use miette::miette as format_err; use { crate::{generator::Generator, os::Fs}, clap::Parser, - color_eyre::eyre::Context, + miette::{Context, IntoDiagnostic}, std::path::Path, tracing::{debug, enabled}, tracing_error::ErrorLayer, tracing_subscriber::prelude::*, }; -pub type Result = color_eyre::Result; +pub type Result = miette::Result; + pub(crate) const DOCS_URL: &str = "https://kg.cartera-mesh.com"; fn init_tracing(debug: bool, trace_agent: Option<&str>) { @@ -79,7 +81,7 @@ async fn init(fs: &Fs, gen_dir: impl AsRef) -> Result<()> { let gen_dir = gen_dir.as_ref(); let kg_config = gen_dir.join("kg.kdl"); if fs.exists(&kg_config) { - return Err(color_eyre::eyre::format_err!( + return Err(format_err!( "kg.kdl already exists at {}", kg_config.display() )); @@ -88,6 +90,7 @@ async fn init(fs: &Fs, gen_dir: impl AsRef) -> Result<()> { if !fs.exists(gen_dir) { fs.create_dir_all(gen_dir) .await + .into_diagnostic() .wrap_err_with(|| format!("failed to create directory {}", gen_dir.display()))?; } @@ -102,6 +105,7 @@ async fn init(fs: &Fs, gen_dir: impl AsRef) -> Result<()> { let dest = gen_dir.join(filename); fs.write(&dest, content) .await + .into_diagnostic() .wrap_err_with(|| format!("failed to write {}", dest.display()))?; println!("Created {}", dest.display()); } @@ -117,7 +121,6 @@ async fn init(fs: &Fs, gen_dir: impl AsRef) -> Result<()> { #[tokio::main] async fn main() -> Result<()> { - color_eyre::install()?; let cli = commands::Cli::parse(); if matches!(cli.command, commands::Command::Version) { println!("{}", clap::crate_version!()); @@ -149,7 +152,9 @@ async fn main() -> Result<()> { "changing working directory to {}", home_dir.as_os_str().display() ); - std::env::set_current_dir(&home_dir)?; + std::env::set_current_dir(&home_dir) + .into_diagnostic() + .wrap_err(format!("failed to set CWD {}", home_dir.display()))?; } if local_mode { span.record("local_mode", true); @@ -173,7 +178,9 @@ async fn main() -> Result<()> { if enabled!(tracing::Level::TRACE) { tracing::trace!( "Loaded Agent Generator Config:\n{}", - serde_json::to_string_pretty(&q_generator_config)? + serde_json::to_string_pretty(&q_generator_config) + .into_diagnostic() + .wrap_err("unable to decode to json")? ); } diff --git a/src/output.rs b/src/output.rs index c4afbc7..a005546 100644 --- a/src/output.rs +++ b/src/output.rs @@ -6,6 +6,7 @@ use { source::KdlSources, }, colored::Colorize, + miette::{Context, IntoDiagnostic}, std::fmt::Display, super_table::{modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, *}, tracing::enabled, @@ -289,7 +290,12 @@ impl OutputFormat { } Self::Json => { let kiro_agents: Vec = results.into_iter().map(|a| a.kiro_agent).collect(); - println!("{}", serde_json::to_string_pretty(&kiro_agents)?); + println!( + "{}", + serde_json::to_string_pretty(&kiro_agents) + .into_diagnostic() + .wrap_err("todo")? + ); Ok(()) } } From 9ae25662d870173164ee4a0bee5101760cdfa598 Mon Sep 17 00:00:00 2001 From: dougeEfresh Date: Mon, 5 Jan 2026 20:28:20 +0000 Subject: [PATCH 5/8] use vec --- Cargo.lock | 89 +++++++++++++++++++++----------------- src/agent/mod.rs | 4 +- src/agent/wrapper_types.rs | 49 --------------------- src/config.rs | 18 ++++---- src/config/agent.rs | 20 +++++---- src/config/agent_file.rs | 4 +- src/config/mcp.rs | 22 +++++----- src/config/merge.rs | 19 ++++---- src/config/native.rs | 29 ++++++------- src/output.rs | 9 +++- 10 files changed, 114 insertions(+), 149 deletions(-) delete mode 100644 src/agent/wrapper_types.rs diff --git a/Cargo.lock b/Cargo.lock index 3244b63..01b1168 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,9 +117,9 @@ dependencies = [ [[package]] name = "arborium" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb0f3ba8f3bc4322ff8c97a0a2090a4bebf358406ea375c7a17d8954f2362a4" +checksum = "94a0fe8162cd3c0c0a6917d3547a99cf64fb3419754edc3490e61dbcb725d931" dependencies = [ "arborium-highlight", "arborium-json", @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "arborium-highlight" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "975a3508e6b26b013345c7c203ee36c81d03f9d30ae4e55dec4b6079c205c6db" +checksum = "2b21b7db1ac1b9b05203cc109a2c547c8ea205e9fd25bcf4f7d59541dcd56d7a" dependencies = [ "arborium-theme", "arborium-tree-sitter", @@ -142,9 +142,9 @@ dependencies = [ [[package]] name = "arborium-json" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9496b97a3b4a83548e81ca1e30a26d14ba1239091d842c72729df2a0d6cb774c" +checksum = "2af5484f35d971c21b37c644f19149531cf1d56f6be453744c6ce2bbc3a6d7df" dependencies = [ "arborium-sysroot", "cc", @@ -153,9 +153,9 @@ dependencies = [ [[package]] name = "arborium-rust" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b18ff2b7196273ba33fc35daf2c409f65bf3df7c3a25b11cbd328ab39ee57c1e" +checksum = "3679029fbf1dfc742eee00c6e7cabfa58636fe33209a3d3356c48fbb18f739a7" dependencies = [ "arborium-sysroot", "cc", @@ -164,9 +164,9 @@ dependencies = [ [[package]] name = "arborium-sysroot" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d83ed18d08e513f5d80bd0f7ed2b3a2369ddbb49d51d674025d94be8c2b9ed8" +checksum = "57c7c907513f0fcf989c9d8945fe4d5efd9031243a19789db3f62bf735ec8179" dependencies = [ "cc", "dlmalloc", @@ -174,15 +174,15 @@ dependencies = [ [[package]] name = "arborium-theme" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dbced2fec10201488d18dbac859f96d479dc089e3caa7f9f2710d199c08604" +checksum = "3800aeb6bb75bb6cb9471c9b845e0a29d58f8a18890f1f248c1c6954d9ea5d8e" [[package]] name = "arborium-tree-sitter" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f086fd94acc289a46b431a8c2a9eba075000e94837e17c647dae43551225d40f" +checksum = "d97dc3525c71cf41e976529b3904da514fb1ec469acb9710b5cf2be5979a2a57" dependencies = [ "arborium-sysroot", "cc", @@ -378,7 +378,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -560,7 +560,7 @@ dependencies = [ [[package]] name = "facet" version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" dependencies = [ "autocfg", "facet-core", @@ -571,7 +571,7 @@ dependencies = [ [[package]] name = "facet-core" version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" dependencies = [ "autocfg", "impls", @@ -580,7 +580,7 @@ dependencies = [ [[package]] name = "facet-format" version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" dependencies = [ "facet-core", "facet-path", @@ -593,7 +593,7 @@ dependencies = [ [[package]] name = "facet-kdl" version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" dependencies = [ "facet", "facet-core", @@ -607,7 +607,7 @@ dependencies = [ [[package]] name = "facet-macro-parse" version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" dependencies = [ "facet-macro-types", "proc-macro2", @@ -617,7 +617,7 @@ dependencies = [ [[package]] name = "facet-macro-types" version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" dependencies = [ "proc-macro2", "quote", @@ -627,7 +627,7 @@ dependencies = [ [[package]] name = "facet-macros" version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" dependencies = [ "facet-macros-impl", ] @@ -635,7 +635,7 @@ dependencies = [ [[package]] name = "facet-macros-impl" version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" dependencies = [ "facet-macro-parse", "facet-macro-types", @@ -648,7 +648,7 @@ dependencies = [ [[package]] name = "facet-path" version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" dependencies = [ "arborium", "facet-core", @@ -660,7 +660,7 @@ dependencies = [ [[package]] name = "facet-pretty" version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" dependencies = [ "facet-core", "facet-reflect", @@ -670,7 +670,7 @@ dependencies = [ [[package]] name = "facet-reflect" version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" dependencies = [ "facet-core", "miette", @@ -679,12 +679,12 @@ dependencies = [ [[package]] name = "facet-singularize" version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" [[package]] name = "facet-solver" version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#d2491664f6bd33401f3047f0800915bd948ae696" +source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" dependencies = [ "facet-core", "facet-reflect", @@ -892,9 +892,9 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -1395,9 +1395,9 @@ dependencies = [ [[package]] name = "miette-arborium" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e011023c9185a2e63af9ba02d09736a0935596225f710f3e279101154c507" +checksum = "03b12314e8eee2a5b4ba79bef82123bacc5c656e8b89e456780ab7c710cf3148" dependencies = [ "arborium", "arborium-highlight", @@ -1914,9 +1914,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "once_cell", "rustls-pki-types", @@ -2158,9 +2158,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" -version = "2.0.112" +version = "2.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" +checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" dependencies = [ "proc-macro2", "quote", @@ -2369,9 +2369,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2793,6 +2793,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -3077,6 +3086,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.8" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317f17ff091ac4515f17cc7a190d2769a8c9a96d227de5d64b500b01cda8f2cd" +checksum = "dcb2c125bd7365735bebeb420ccb880265ed2d2bddcbcd49f597fdfe6bd5e577" diff --git a/src/agent/mod.rs b/src/agent/mod.rs index c91c3fc..d5e1c3f 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -2,7 +2,6 @@ mod custom_tool; pub mod hook; mod mcp_config; pub mod tools; -mod wrapper_types; pub const DEFAULT_AGENT_RESOURCES: &[&str] = &["file://README.md", "file://AGENTS.md"]; pub const DEFAULT_APPROVE: [&str; 0] = []; use { @@ -19,7 +18,6 @@ pub use { custom_tool::{CustomToolConfig, tool_default_timeout}, mcp_config::McpServerConfig, tools::*, - wrapper_types::OriginalToolName, }; #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] @@ -47,7 +45,7 @@ pub struct Agent { pub tools: HashSet, /// Tool aliases for remapping tool names #[serde(default)] - pub tool_aliases: HashMap, + pub tool_aliases: HashMap, /// List of tools the agent is explicitly allowed to use #[serde(default)] pub allowed_tools: HashSet, diff --git a/src/agent/wrapper_types.rs b/src/agent/wrapper_types.rs deleted file mode 100644 index 7b6d7e1..0000000 --- a/src/agent/wrapper_types.rs +++ /dev/null @@ -1,49 +0,0 @@ -use { - serde::{Deserialize, Serialize}, - std::{borrow::Borrow, hash::Hash, ops::Deref}, -}; - -/// Subject of the tool name change. For tools in mcp servers, you would need to -/// prefix them with their server names -#[derive(Debug, Clone, Serialize, Deserialize, Eq, Hash, PartialEq)] -pub struct OriginalToolName(pub String); - -impl Deref for OriginalToolName { - type Target = String; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Borrow for OriginalToolName { - fn borrow(&self) -> &str { - self.0.as_str() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn original_tool_name_deref() { - let name = OriginalToolName("test".into()); - assert_eq!(&*name, "test"); - } - - #[test] - fn original_tool_name_borrow() { - let name = OriginalToolName("test".into()); - let borrowed: &str = name.borrow(); - assert_eq!(borrowed, "test"); - } - - #[test] - fn original_tool_name_serde() { - let name = OriginalToolName("test".into()); - let json = serde_json::to_string(&name).unwrap(); - let deserialized: OriginalToolName = serde_json::from_str(&json).unwrap(); - assert_eq!(name, deserialized); - } -} diff --git a/src/config.rs b/src/config.rs index 9419fd3..9080fbf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -41,28 +41,30 @@ impl AsRef for GenericItem { #[derive(Facet, Debug, Default, PartialEq, Clone, Eq)] #[facet(default)] -pub(super) struct GenericItemList { +pub(super) struct GenericSet { #[facet(kdl::arguments)] pub item: HashSet, } -impl Display for GenericItemList { +impl Display for GenericSet { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.item) } } -impl AsRef> for GenericItemList { +impl AsRef> for GenericSet { fn as_ref(&self) -> &HashSet { &self.item } } -impl GenericItemList { - fn len(&self) -> usize { - self.item.len() - } -} +// #[cfg(test)] +// impl GenericSet { +// #[cfg(test)] +// fn len(&self) -> usize { +// self.item.len() +// } +// } #[derive(Facet, Debug, Default, PartialEq, Clone, Eq)] #[facet(default)] diff --git a/src/config/agent.rs b/src/config/agent.rs index d192ea7..165b345 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -6,8 +6,8 @@ use { native::{AwsTool, ExecuteShellTool, NativeTools, NativeToolsDoc, ReadTool, WriteTool}, }, crate::{ - agent::{CustomToolConfig, OriginalToolName}, - config::split_newline, + agent::CustomToolConfig, + config::{GenericVec, split_newline}, }, facet::Facet, facet_kdl as kdl, @@ -75,7 +75,7 @@ pub struct KdlAgent { pub model: Option, pub hook: HookPart, pub mcp: HashMap, - pub alias: HashMap, + pub alias: HashMap, pub native_tool: NativeTools, pub tool_setting: Vec, } @@ -120,7 +120,7 @@ pub struct KdlAgentDoc { pub(super) mcp: Vec, #[facet(kdl::children, default)] - pub(super) alias: Vec, + pub(super) alias: Vec, #[facet(kdl::child, default)] pub native_tool: NativeToolsDoc, @@ -212,11 +212,13 @@ impl KdlAgentDoc { self.template.is_some_and(|f| f) } - pub fn tool_aliases(&self) -> HashMap { - self.alias - .iter() - .map(|m| (OriginalToolName(m.from.clone()), m.to.clone())) - .collect() + pub fn tool_aliases(&self) -> HashMap { + let mut map: HashMap = HashMap::new(); + for a in &self.alias { + let m = HashMap::from(a.clone()); + map.extend(m); + } + map } pub fn hooks(&self) -> HookPart { diff --git a/src/config/agent_file.rs b/src/config/agent_file.rs index 2b7d0c9..ab0038c 100644 --- a/src/config/agent_file.rs +++ b/src/config/agent_file.rs @@ -6,7 +6,7 @@ use { mcp::CustomToolConfigDoc, native::NativeToolsDoc, }, - crate::Fs, + crate::{Fs, config::GenericVec}, facet::Facet, facet_kdl as kdl, miette::{Context, IntoDiagnostic}, @@ -48,7 +48,7 @@ pub struct KdlAgentFileDoc { #[facet(kdl::children, default)] pub(super) mcp: Vec, #[facet(kdl::children, default)] - pub(super) alias: Vec, + pub(super) alias: Vec, #[facet(kdl::child, default)] pub native_tool: NativeToolsDoc, #[facet(kdl::children, default)] diff --git a/src/config/mcp.rs b/src/config/mcp.rs index c0f38fd..7655b3d 100644 --- a/src/config/mcp.rs +++ b/src/config/mcp.rs @@ -1,11 +1,7 @@ use { - crate::{ - agent::CustomToolConfig, - config::{GenericItemList, GenericVec}, - }, + crate::{agent::CustomToolConfig, config::GenericVec}, facet::Facet, facet_kdl as kdl, - std::collections::HashMap, }; #[derive(Facet, Clone, Debug)] @@ -29,7 +25,7 @@ pub struct CustomToolConfigDoc { pub command: String, #[facet(kdl::child, default)] - args: GenericItemList, + args: GenericVec, #[facet(kdl::child, default)] env: GenericVec, @@ -74,6 +70,7 @@ mod tests { super::*, crate::config::{ConfigResult, kdl_parse}, indoc::indoc, + std::collections::HashMap, }; #[derive(Facet, Debug)] @@ -130,8 +127,14 @@ mod tests { let header: HashMap = doc.mcp.header.into(); assert_eq!(header.len(), 2); - assert_eq!(header.get("Authorization"), Some(&"Bearer token".to_string())); - assert_eq!(header.get("Content-Type"), Some(&"application/json".to_string())); + assert_eq!( + header.get("Authorization"), + Some(&"Bearer token".to_string()) + ); + assert_eq!( + header.get("Content-Type"), + Some(&"application/json".to_string()) + ); Ok(()) } @@ -144,8 +147,7 @@ mod tests { }; let doc: McpDoc = kdl_parse(kdl)?; - let args: Vec = doc.mcp.args.item.into_iter().collect(); - assert_eq!(args, vec!["--verbose", "--output=json"]); + assert_eq!(doc.mcp.args.item, vec!["--verbose", "--output=json"]); Ok(()) } diff --git a/src/config/merge.rs b/src/config/merge.rs index 99b037b..b1d351d 100644 --- a/src/config/merge.rs +++ b/src/config/merge.rs @@ -44,10 +44,10 @@ mod tests { tool "shell" native-tool { write { - override "Cargo.lock" + overrides "Cargo.lock" } shell { - override "git push .*" + overrides "git push .*" } } hook { @@ -72,20 +72,17 @@ mod tests { alias "fs_read" "read" native-tool { read { - allow "./src/*" - allow "./scripts/**" - deny "Cargo.lock" + allows "./src/*" "./scripts/**" + denies "Cargo.lock" } write { - allow "./src/*" - allow "./scripts/**" - deny "Cargo.lock" + allows "./src/*" "./scripts/**" + denies "Cargo.lock" } shell { - allow "git status .*" - allow "git pull .*" - deny "git push .*" + allows "git status .*" "git pull .*" + denies "git push .*" } } hook { diff --git a/src/config/native.rs b/src/config/native.rs index 593e255..0fd281c 100644 --- a/src/config/native.rs +++ b/src/config/native.rs @@ -1,5 +1,5 @@ use { - super::GenericItemList, + super::GenericSet, crate::agent::{ AwsTool as KiroAwsTool, ExecuteShellTool as KiroShellTool, @@ -42,11 +42,11 @@ macro_rules! define_kdl_doc { #[facet(default, rename_all = "kebab-case")] pub struct $name { #[facet(default, kdl::child)] - pub(super) allows: GenericItemList, + pub(super) allows: GenericSet, #[facet(default, kdl::child)] - pub(super) denies: GenericItemList, + pub(super) denies: GenericSet, #[facet(default, kdl::child)] - pub(super) overrides: GenericItemList, + pub(super) overrides: GenericSet, #[facet(default, kdl::property)] pub deny_by_default: Option, #[facet(default, kdl::property)] @@ -85,6 +85,7 @@ define_tool_into!(WriteToolDoc, WriteTool); define_tool_into!(ReadToolDoc, ReadTool); #[derive(Facet, Default, Clone, Debug, PartialEq, Eq)] +#[facet(default)] pub struct NativeToolsDoc { #[facet(default, kdl::child)] pub shell: ExecuteShellToolDoc, @@ -273,9 +274,8 @@ mod tests { fn parse_aws_tool() -> ConfigResult<()> { let kdl = r#" aws disable-auto-readonly=#true { - allow "ec2" - allow "s3" - deny "iam" + allows "ec2" "s3" + denies "iam" } "#; @@ -292,17 +292,14 @@ mod tests { fn parse_read_write_tools() -> ConfigResult<()> { let kdl = r#" read { - allow """ - *.rs - *.toml - """ - deny "/etc/*" - override "/etc/hosts" + allows "*.rs" "*.toml" + denies "/etc/*" + overrides "/etc/hosts" } write { - allow "*.txt" - deny "/tmp/*" - override "/tmp/allowed" + allows "*.txt" + denies "/tmp/*" + overrides "/tmp/allowed" } "#; diff --git a/src/output.rs b/src/output.rs index a005546..6f27fd9 100644 --- a/src/output.rs +++ b/src/output.rs @@ -6,12 +6,19 @@ use { source::KdlSources, }, colored::Colorize, - miette::{Context, IntoDiagnostic}, + miette::{Context, GraphicalReportHandler, GraphicalTheme, IntoDiagnostic}, std::fmt::Display, super_table::{modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, *}, tracing::enabled, }; +pub fn print_error(color: &ColorOverride, e: &facet_kdl::KdlDeserializeError) { + let mut output = String::new(); + let handler = GraphicalReportHandler::new_themed(GraphicalTheme::unicode()); + handler.render_report(&mut output, e).unwrap(); + eprintln!("{}", output); +} + /// Override the color setting. Default is [`ColorOverride::Auto`]. #[derive(Copy, Clone, Debug, Default, clap::ValueEnum)] pub enum ColorOverride { From fe35c62e510ae1761ccbb5d372b5ddb3551611a1 Mon Sep 17 00:00:00 2001 From: dougeEfresh Date: Wed, 7 Jan 2026 07:27:13 +0000 Subject: [PATCH 6/8] format --- .kiro/generators/base.kdl | 47 +++++------------------- Cargo.lock | 76 +++++++++++++++++++-------------------- src/config.rs | 5 +-- src/config/agent.rs | 36 +++++++++---------- src/config/agent_file.rs | 59 ++++++++++++++++-------------- src/generator/discover.rs | 12 +++---- 6 files changed, 106 insertions(+), 129 deletions(-) diff --git a/.kiro/generators/base.kdl b/.kiro/generators/base.kdl index 8394619..67b321c 100644 --- a/.kiro/generators/base.kdl +++ b/.kiro/generators/base.kdl @@ -1,13 +1,9 @@ description "Default agent for Kiro" -tools * -allowed-tools "read" -allowed-tools "knowledge" -allowed-tools "fetch" +tools "*" +allowed-tools "read" "knowledge" "fetch" resource "file://README.md" resource "file://.amazonq/rules/**/*.md" - -alias "execute_shell" bash - +alias "execute_shell" "bash" hook { agent-spawn echo { command "echo My name is Bob" @@ -15,49 +11,24 @@ hook { cache-ttl-seconds 300 } } - native-tool { read { - deny .*Cargo.toml.* - deny .*yarn.lock.* + denies ".*Cargo.toml.*" ".*yarn.lock.*" } write { - deny .*Cargo.toml.* + denies ".*Cargo.toml.*" } shell { - allow """ - git status - git fetch - git diff .* - git pull.* - yarn.* - pulumi preview .* - kubectl .* - pdf2.* - ps .* - timeout.* - pgrep.* - """ - deny """ - git commit .* - git push .* - kubectl .*delete* - .*delete.* - pulumi up.* - ^rm .* - .*destroy.* - .*rollout.* - .*kill.* - """ + allows "git status" "git fetch" "git diff .*" "git pull.*" "yarn.*" "pulumi preview .*" "pdf2.*" "ps .*" "timeout.*" "pgrep.*" + denies "git commit .*" "git push .*" "kubectl .*delete*" ".*delete.*" "pulumi up.*" "^rm .*" ".*destroy.*" ".*rollout.*" ".*kill.*" } } mcp rustdocs { - command mcp-docsrs + command "mcp-docsrs" timeout 1200 } mcp cargo { - command cargo-mcp + command "cargo-mcp" args "--debug" timeout 120000 } - diff --git a/Cargo.lock b/Cargo.lock index 01b1168..1e56489 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -559,8 +559,8 @@ dependencies = [ [[package]] name = "facet" -version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" +version = "0.42.0" +source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" dependencies = [ "autocfg", "facet-core", @@ -570,8 +570,8 @@ dependencies = [ [[package]] name = "facet-core" -version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" +version = "0.42.0" +source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" dependencies = [ "autocfg", "impls", @@ -579,8 +579,8 @@ dependencies = [ [[package]] name = "facet-format" -version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" +version = "0.42.0" +source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" dependencies = [ "facet-core", "facet-path", @@ -592,8 +592,8 @@ dependencies = [ [[package]] name = "facet-kdl" -version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" +version = "0.42.0" +source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" dependencies = [ "facet", "facet-core", @@ -606,8 +606,8 @@ dependencies = [ [[package]] name = "facet-macro-parse" -version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" +version = "0.42.0" +source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" dependencies = [ "facet-macro-types", "proc-macro2", @@ -616,8 +616,8 @@ dependencies = [ [[package]] name = "facet-macro-types" -version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" +version = "0.42.0" +source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" dependencies = [ "proc-macro2", "quote", @@ -626,16 +626,16 @@ dependencies = [ [[package]] name = "facet-macros" -version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" +version = "0.42.0" +source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" dependencies = [ "facet-macros-impl", ] [[package]] name = "facet-macros-impl" -version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" +version = "0.42.0" +source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" dependencies = [ "facet-macro-parse", "facet-macro-types", @@ -647,8 +647,8 @@ dependencies = [ [[package]] name = "facet-path" -version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" +version = "0.42.0" +source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" dependencies = [ "arborium", "facet-core", @@ -659,8 +659,8 @@ dependencies = [ [[package]] name = "facet-pretty" -version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" +version = "0.42.0" +source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" dependencies = [ "facet-core", "facet-reflect", @@ -669,8 +669,8 @@ dependencies = [ [[package]] name = "facet-reflect" -version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" +version = "0.42.0" +source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" dependencies = [ "facet-core", "miette", @@ -678,13 +678,13 @@ dependencies = [ [[package]] name = "facet-singularize" -version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" +version = "0.42.0" +source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" [[package]] name = "facet-solver" -version = "0.41.0" -source = "git+https://github.com/facet-rs/facet.git#f39622731f1beb518b8274ee643a67e0c75023da" +version = "0.42.0" +source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" dependencies = [ "facet-core", "facet-reflect", @@ -1723,18 +1723,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -2027,9 +2027,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", @@ -2510,9 +2510,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-general-category" @@ -2569,9 +2569,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -3086,6 +3086,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb2c125bd7365735bebeb420ccb880265ed2d2bddcbcd49f597fdfe6bd5e577" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" diff --git a/src/config.rs b/src/config.rs index 9080fbf..f3bd682 100644 --- a/src/config.rs +++ b/src/config.rs @@ -112,7 +112,8 @@ impl AsRef for IntDoc { } } -fn print_error(name: &str, e: &facet_kdl::KdlDeserializeError) { +#[cfg(test)] +fn print_error(e: &facet_kdl::KdlDeserializeError) { // let d = e.into_diagnostics(); eprintln!("\n=== Miette render ==="); let mut output = String::new(); @@ -151,7 +152,7 @@ where { match kdl::from_str::(content) { Err(e) => { - print_error("test", &e); + print_error(&e); Err(crate::format_err!("{e}")) } Ok(r) => Ok(r), diff --git a/src/config/agent.rs b/src/config/agent.rs index 165b345..37f5409 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -7,7 +7,7 @@ use { }, crate::{ agent::CustomToolConfig, - config::{GenericVec, split_newline}, + config::{GenericSet, GenericVec, split_newline}, }, facet::Facet, facet_kdl as kdl, @@ -90,13 +90,13 @@ pub struct KdlAgentDoc { pub template: Option, #[facet(kdl::child, default)] - pub(super) description: Option, + pub(super) description: Option, - #[facet(kdl::children, default)] - pub(super) inherits: Vec, + #[facet(kdl::child, default)] + pub(super) inherits: GenericSet, #[facet(kdl::child, default)] - pub(super) prompt: Option, + pub(super) prompt: Option, #[facet(kdl::children, default)] pub(super) resources: Vec, @@ -104,14 +104,14 @@ pub struct KdlAgentDoc { #[facet(kdl::property, default)] pub include_mcp_json: Option, - #[facet(kdl::children, rename = "tools", default)] - pub(super) tools: Vec, + #[facet(kdl::child, rename = "tools", default)] + pub(super) tools: GenericSet, - #[facet(kdl::children, rename = "allowed_tools", default)] - pub(super) allowed_tools: Vec, + #[facet(kdl::child, default)] + pub(super) allowed_tools: GenericSet, #[facet(kdl::child, default)] - pub(super) model: Option, + pub(super) model: Option, #[facet(kdl::child, default)] pub(super) hook: Option, @@ -145,8 +145,8 @@ impl From for KdlAgent { fn from(value: KdlAgentDoc) -> Self { Self { name: value.name.clone(), - description: value.description.as_ref().map(|f| f.item.clone()), - prompt: value.prompt.as_ref().map(|f| f.item.clone()), + description: value.description.clone(), + prompt: value.prompt.clone(), alias: value.tool_aliases(), allowed_tools: value.allowed_tools(), inherits: value.inherits(), @@ -154,7 +154,7 @@ impl From for KdlAgent { include_mcp_json: value.include_mcp_json, hook: value.hooks(), resources: value.resources(), - model: value.model.as_ref().map(|f| f.item.clone()), + model: value.model.clone(), mcp: value.mcp_servers(), tools: value.tools(), tool_setting: Default::default(), // TODO use facet::Value @@ -194,11 +194,11 @@ impl KdlAgent { impl KdlAgentDoc { pub fn prompt(&self) -> String { - self.prompt.clone().unwrap_or_default().item + self.prompt.clone().unwrap_or_default() } pub fn description(&self) -> String { - self.description.clone().unwrap_or_default().item + self.description.clone().unwrap_or_default() } pub fn new(name: impl AsRef) -> Self { @@ -226,15 +226,15 @@ impl KdlAgentDoc { } pub fn allowed_tools(&self) -> HashSet { - split_newline(self.allowed_tools.clone()) + self.allowed_tools.item.clone() } pub fn tools(&self) -> HashSet { - split_newline(self.tools.clone()) + self.tools.item.clone() } pub fn inherits(&self) -> HashSet { - split_newline(self.inherits.clone()) + self.inherits.item.clone() } pub fn resources(&self) -> HashSet { diff --git a/src/config/agent_file.rs b/src/config/agent_file.rs index ab0038c..ae421e9 100644 --- a/src/config/agent_file.rs +++ b/src/config/agent_file.rs @@ -1,15 +1,18 @@ use { super::{ GenericItem, + GenericSet, agent::*, hook::HookDoc, mcp::CustomToolConfigDoc, native::NativeToolsDoc, }, - crate::{Fs, config::GenericVec}, + crate::{ + Fs, + config::{ConfigResult, GenericVec}, + }, facet::Facet, facet_kdl as kdl, - miette::{Context, IntoDiagnostic}, std::path::Path, }; @@ -23,34 +26,41 @@ pub(super) struct BoolDoc { #[facet(deny_unknown_fields, rename_all = "kebab-case", default)] pub struct KdlAgentFileDoc { #[facet(kdl::child, default)] - pub(super) description: Option, - #[facet(kdl::children, default)] - pub(super) inherits: Vec, + pub(super) description: Option, + #[facet(kdl::child, default)] - pub(super) prompt: Option, - #[facet(kdl::children, default)] - pub(super) resources: Vec, + pub(super) inherits: GenericSet, #[facet(kdl::child, default)] - pub(super) include_mcp_json: Option, + pub(super) prompt: Option, #[facet(kdl::children, default)] - pub(super) tools: Vec, + pub(super) resources: Vec, - #[facet(kdl::children, default)] - pub(super) allowed_tools: Vec, + #[facet(kdl::property, default)] + pub include_mcp_json: Option, + + #[facet(kdl::child, rename = "tools", default)] + pub(super) tools: GenericSet, #[facet(kdl::child, default)] - pub(super) model: Option, + pub(super) allowed_tools: GenericSet, + + #[facet(kdl::child, default)] + pub(super) model: Option, #[facet(kdl::child, default)] pub(super) hook: Option, + #[facet(kdl::children, default)] pub(super) mcp: Vec, + #[facet(kdl::children, default)] pub(super) alias: Vec, + #[facet(kdl::child, default)] pub native_tool: NativeToolsDoc, + #[facet(kdl::children, default)] pub(super) tool_setting: Vec, } @@ -60,19 +70,14 @@ impl KdlAgentDoc { fs: &Fs, name: impl AsRef, path: impl AsRef, - ) -> crate::Result> { - if !fs.exists(&path) { - return Ok(None); - } - - let content = fs - .read_to_string_sync(&path) - .into_diagnostic() - .wrap_err(format!("unable to read {}", path.as_ref().display()))?; - let agent: KdlAgentFileDoc = kdl::from_str(&content).into_diagnostic().map_err(|e| { - crate::format_err!("failed {} error:'{e}'\n{content}", path.as_ref().display()) - })?; - Ok(Some(Self::from_file_source(name, agent))) + ) -> Option> { + if let Some(result) = super::kdl_parse_path::(fs, path) { + match result { + Err(e) => return Some(Err(e)), + Ok(file_source) => return Some(Ok(Self::from_file_source(name, file_source))), + } + }; + None } pub fn from_file_source(name: impl AsRef, file_source: KdlAgentFileDoc) -> Self { @@ -83,7 +88,7 @@ impl KdlAgentDoc { inherits: file_source.inherits, prompt: file_source.prompt, resources: file_source.resources, - include_mcp_json: Some(file_source.include_mcp_json.unwrap_or_default().value), + include_mcp_json: file_source.include_mcp_json, tools: file_source.tools, allowed_tools: file_source.allowed_tools, model: file_source.model, diff --git a/src/generator/discover.rs b/src/generator/discover.rs index f4ee8b9..aa467e4 100644 --- a/src/generator/discover.rs +++ b/src/generator/discover.rs @@ -23,12 +23,12 @@ fn process_local( sources: &mut Vec, ) -> Result { let local_agent_path = location.local(&name); - let result = KdlAgentDoc::from_path(fs, &name, &local_agent_path)?; + let result = KdlAgentDoc::from_path(fs, &name, &local_agent_path); match result { None => Ok(KdlAgent::new(name.as_ref().to_string())), Some(a) => { + let agent = KdlAgent::from(a?.clone()); sources.push(KdlAgentSource::LocalFile(local_agent_path)); - let agent = KdlAgent::from(a.clone()); if let Some(i) = inline { sources.push(KdlAgentSource::LocalInline); Ok(agent.merge(i.clone())) @@ -123,20 +123,20 @@ pub fn discover( agent_sources.push(KdlAgentSource::GlobalInline); result = result.merge(a.clone()); } - let maybe_global_file = KdlAgentDoc::from_path(fs, name, location.global(name))?; + let maybe_global_file = KdlAgentDoc::from_path(fs, name, location.global(name)); if let Some(global) = maybe_global_file { agent_sources.push(KdlAgentSource::GlobalFile(location.global(name))); - result = result.merge(KdlAgent::from(global.clone())); + result = result.merge(KdlAgent::from(global?.clone())); } resolved_agents.insert(name.to_string(), result); } ConfigLocation::Global(_) => { - let mut global_file = match KdlAgentDoc::from_path(fs, name, location.global(name))? + let mut global_file = match KdlAgentDoc::from_path(fs, name, location.global(name)) { None => KdlAgent::new(name.to_string()), Some(a) => { agent_sources.push(KdlAgentSource::GlobalFile(location.global(name))); - KdlAgent::from(a) + KdlAgent::from(a?) } }; if let Some(inline) = global_agents.get(name) { From dc874c68c4aab8943a7d41daf56dee1ae5b90ee1 Mon Sep 17 00:00:00 2001 From: dougeEfresh Date: Fri, 9 Jan 2026 12:17:23 +0000 Subject: [PATCH 7/8] feat: error message report --- .kiro/generators/aws-test.kdl | 30 +++--- .kiro/generators/bad.kdl | 3 +- .kiro/generators/base.kdl | 21 +++- .kiro/generators/dependabot.kdl | 18 ++-- .kiro/global/aws-test.kdl | 17 ++- AGENTS.md | 176 +------------------------------- Cargo.lock | 68 +++++++----- Cargo.toml | 19 ++-- src/agent/mcp_config.rs | 39 ------- src/agent/mod.rs | 8 +- src/config.rs | 25 ++--- src/config/agent_file.rs | 2 +- src/config/merge.rs | 12 +-- src/config/native.rs | 49 +++++---- src/error.rs | 5 + src/generator/merge.rs | 174 +++++++++++++++---------------- src/main.rs | 3 +- src/output.rs | 14 ++- 18 files changed, 256 insertions(+), 427 deletions(-) delete mode 100644 src/agent/mcp_config.rs create mode 100644 src/error.rs diff --git a/.kiro/generators/aws-test.kdl b/.kiro/generators/aws-test.kdl index c675e4b..0382618 100644 --- a/.kiro/generators/aws-test.kdl +++ b/.kiro/generators/aws-test.kdl @@ -1,11 +1,9 @@ description "all the AWS tools you want" prompt "you are an AWS expert" -allowed-tools "@awsdocs" - +allowed-tools "@awsdocs" resource "file://AGENTS.md" resource "file://README.md" resource "file://.amazonq/rules/**/*.md" - hook { agent-spawn "whoami" { command "aws sts get-caller-identity" @@ -13,26 +11,22 @@ hook { cache-ttl-seconds 300 } } - native-tool { - aws { - allow "ec2" - allow "s3" - deny "iam" - } + aws { + allows "ec2" "s3" + denies "iam" + } } - mcp "awsbilling" { - command "uvx" - args """ + command "uvx" + args """ awslabs.billing-cost-management-mcp-server@latest """ - env "FASTMCP_LOG_LEVEL" "ERROR" + env "FASTMCP_LOG_LEVEL" "ERROR" } - mcp "awsdocs" { - command "uvx" - args "awslabs.aws-documentation-mcp-server@latest" - env "FASTMCP_LOG_LEVEL" "ERROR" - env "AWS_DOCUMENTATION_PARTITION" "aws" + command "uvx" + args "awslabs.aws-documentation-mcp-server@latest" + env "FASTMCP_LOG_LEVEL" "ERROR" + env "AWS_DOCUMENTATION_PARTITION" "aws" } diff --git a/.kiro/generators/bad.kdl b/.kiro/generators/bad.kdl index 5635c74..d8a53e0 100644 --- a/.kiro/generators/bad.kdl +++ b/.kiro/generators/bad.kdl @@ -1 +1,2 @@ -foo "bar" +agent "bad" include-mcp-json="bar" { +} diff --git a/.kiro/generators/base.kdl b/.kiro/generators/base.kdl index 67b321c..d86c3ac 100644 --- a/.kiro/generators/base.kdl +++ b/.kiro/generators/base.kdl @@ -19,8 +19,25 @@ native-tool { denies ".*Cargo.toml.*" } shell { - allows "git status" "git fetch" "git diff .*" "git pull.*" "yarn.*" "pulumi preview .*" "pdf2.*" "ps .*" "timeout.*" "pgrep.*" - denies "git commit .*" "git push .*" "kubectl .*delete*" ".*delete.*" "pulumi up.*" "^rm .*" ".*destroy.*" ".*rollout.*" ".*kill.*" + allows "git status" \ + "git fetch" \ + "git diff .*" \ + "git pull.*" \ + "yarn.*" \ + "pulumi preview .*" \ + "pdf2.*" \ + "ps .*" \ + "timeout.*" \ + "pgrep.*" + denies "git commit .*" \ + "git push .*" \ + "kubectl .*delete*" \ + ".*delete.*" \ + "pulumi up.*" \ + "^rm .*" \ + ".*destroy.*" \ + ".*rollout.*" \ + ".*kill.*" } } mcp rustdocs { diff --git a/.kiro/generators/dependabot.kdl b/.kiro/generators/dependabot.kdl index ed7f43d..cada853 100644 --- a/.kiro/generators/dependabot.kdl +++ b/.kiro/generators/dependabot.kdl @@ -1,8 +1,12 @@ description "I make life painful for developers" - -// native-tool { -// shell { override "git commit .*"; override "git push .*"; } -// read { override ".*Cargo.toml.*"; } -// write { override ".*Cargo.toml.*"; } -// // aws disable-auto-readonly=true -//} +native-tool disable-auto-readonly=#true { + shell { + overrides "git commit .*" "git push .*" + } + read { + overrides ".*Cargo.toml.*" + } + write { + overrides ".*Cargo.toml.*" + } +} diff --git a/.kiro/global/aws-test.kdl b/.kiro/global/aws-test.kdl index 59202ec..839d564 100644 --- a/.kiro/global/aws-test.kdl +++ b/.kiro/global/aws-test.kdl @@ -1,16 +1,15 @@ prompt "you are NOT an AWS expert" -allowed-tools "@awsdocs" +allowed-tools "@awsdocs" resource "file://AGENTS.md" resource "file://README.md" native-tool { - aws { - allow "ec2" - allow "s3" - } + aws { + allows "ec2" "s3" + } } mcp "awsdocs" { - command "blah" - args "awslabs.aws-documentation-mcp-server@latest" - env "FASTMCP_LOG_LEVEL" "INFO" - env "AWS_DOCUMENTATION_PARTITION" "ec2" + command "blah" + args "awslabs.aws-documentation-mcp-server@latest" + env "FASTMCP_LOG_LEVEL" "INFO" + env "AWS_DOCUMENTATION_PARTITION" "ec2" } diff --git a/AGENTS.md b/AGENTS.md index 6778b77..c2d3c19 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,180 +86,10 @@ When analyzing the code, explicitly check: - Explanation of *why* each change aligns with Unix conventions - Prioritize changes: critical issues first, nice-to-haves last -## Error Handling with color_eyre -This project uses [color_eyre](https://docs.rs/color-eyre/latest/color_eyre/) for enhanced error reporting and diagnostics. +## Error Handling with miette -### Setup & Integration - -**Initialization:** -- Call `color_eyre::install()?` early in `main()` before any fallible operations -- Consider using `color_eyre::config::HookBuilder` for custom configuration -- Disable colors in non-TTY environments or when `NO_COLOR` is set - -**Example:** -```rust -fn main() -> color_eyre::Result<()> { - color_eyre::config::HookBuilder::default() - .display_env_section(false) // Hide environment vars in production - .install()?; - - // Your CLI logic here -} -``` - -### Error Context Best Practices - -**Use `.wrap_err()` and `.wrap_err_with()` liberally:** -- Add context at each layer where it's meaningful -- Focus on *what* the code was trying to do, not *why* it failed (eyre handles that) -- Provide user-actionable information when possible - -**Good context:** -```rust -fs::read_to_string(&config_path) - .wrap_err_with(|| format!("Failed to read config file at {}", config_path.display())) - .wrap_err("Unable to load application configuration")?; -``` - -**Poor context:** -```rust -fs::read_to_string(&config_path) - .wrap_err("Error reading file")?; // Too vague -``` - -### Context Patterns - -**File operations:** -```rust -.wrap_err_with(|| format!("Failed to read '{}'", path.display())) -.wrap_err_with(|| format!("Failed to write to '{}'", path.display())) -.wrap_err_with(|| format!("Failed to create directory '{}'", path.display())) -``` - -**Network/external operations:** -```rust -.wrap_err_with(|| format!("Failed to fetch data from {}", url)) -.wrap_err("Unable to connect to remote service") -``` - -**Parsing/validation:** -```rust -.wrap_err_with(|| format!("Invalid configuration in '{}'", config_path.display())) -.wrap_err_with(|| format!("Failed to parse {} as JSON", file_name)) -``` - -**User input:** -```rust -.wrap_err("Invalid argument provided") -.wrap_err_with(|| format!("Unknown format '{}'. Valid formats: json, yaml, csv", format)) -``` - -### Error Reporting Guidelines - -**For production CLIs:** -- Set `RUST_BACKTRACE=0` behavior as default -- Only show full backtraces in debug builds or with `--verbose` -- Consider using `HookBuilder` to customize what's displayed: -```rust - color_eyre::config::HookBuilder::default() - .display_env_section(cfg!(debug_assertions)) - .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new")) - .install()?; -``` - -**Error message structure:** -- Top-level context should explain the high-level operation that failed -- Each `.wrap_err()` adds a layer showing the call stack conceptually -- The root cause (from the original error) appears at the bottom -- Suggestions and help text should go in the outermost context - -**User-facing vs. developer errors:** -- Configuration errors, invalid input → Detailed, actionable messages -- Unexpected errors, bugs → Include issue tracker URL via `HookBuilder::issue_url()` -- Network timeouts, connection failures → Suggest retry, check connectivity - -### Common Pitfalls to Avoid - -❌ **Don't add redundant context:** -```rust -// The error already says "No such file or directory" -.wrap_err("File not found")? -``` - -❌ **Don't expose internal implementation details:** -```rust -// User doesn't care about your HashMap -.wrap_err("Failed to insert into cache HashMap")? -``` - -❌ **Don't use context for control flow:** -```rust -// Use proper error types for expected failures -match x { - Some(v) => v, - None => return Err(eyre!("Value missing"))?, // ❌ Not an exceptional error -} -``` - -✅ **Do create custom error types for domain errors:** -```rust -#[derive(Debug, thiserror::Error)] -enum ConfigError { - #[error("Missing required field: {0}")] - MissingField(String), - #[error("Invalid port number: {0}")] - InvalidPort(u16), -} - -// Then wrap with color_eyre for context -validate_config(&config) - .wrap_err("Configuration validation failed")?; -``` - -### Integration with Clap - -**Handling clap errors:** -```rust -use clap::Parser; - -#[derive(Parser)] -struct Cli { /* ... */ } - -fn main() -> color_eyre::Result<()> { - color_eyre::install()?; - - // Clap handles its own error formatting, which is good - let cli = Cli::parse(); - - run(cli)?; - Ok(()) -} -``` - -**Adding suggestions to errors:** -```rust -use color_eyre::{eyre::eyre, Help, SectionExt}; - -if !config_path.exists() { - return Err(eyre!("Config file not found")) - .with_suggestion(|| format!("Create a config file at: {}", config_path.display())) - .suggestion("Run with --init to create a default configuration"); -} -``` - -### Review Checklist - -When reviewing error handling: - -1. Is `color_eyre::install()` called early in `main()`? -2. Does each `.wrap_err()` add meaningful context? -3. Are error messages actionable for the user? -4. Are internal implementation details hidden from user-facing errors? -5. Do errors include suggestions where appropriate? -6. Is backtrace/env output appropriate for the audience (debug vs. production)? -7. Are file paths, URLs, and identifiers included in error context? -8. Do network/IO errors guide users toward resolution? +Refer to [miette](https://docs.rs/miette/latest/miette/) for error handles and reporting. Suggest utilizing [Diagnostic](https://docs.rs/miette/latest/miette/trait.Diagnostic.html) trait or enchanced error reporting ## Performance @@ -267,7 +97,7 @@ When reviewing error handling: Runtime Performance is **NOT** critical or important. This CLI will rarely be executed, it is far **MORE** important that the code is clean, maintainable and **SIMPLE** at the cost of performance. -## Further Documentation +## Further Documentation The directory `docs` contains mdbook formatted documentation for the project. Some notiable files: diff --git a/Cargo.lock b/Cargo.lock index 1e56489..67089a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,9 +304,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.51" +version = "1.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ "find-msvc-tools", "shlex", @@ -560,7 +560,8 @@ dependencies = [ [[package]] name = "facet" version = "0.42.0" -source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3244604535a5d0a7c79403f3c6a6184b49be89be99f33c4e8177aebca0167b" dependencies = [ "autocfg", "facet-core", @@ -571,7 +572,8 @@ dependencies = [ [[package]] name = "facet-core" version = "0.42.0" -source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6337f704e6988f9cfee38f5b2c9001e84c498eb70cb420f6dc0e14ca7c1f85eb" dependencies = [ "autocfg", "impls", @@ -580,7 +582,8 @@ dependencies = [ [[package]] name = "facet-format" version = "0.42.0" -source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332a9d31f60d8439a68589331f49e5f058937a9e17d067217f3c87621490753d" dependencies = [ "facet-core", "facet-path", @@ -593,7 +596,8 @@ dependencies = [ [[package]] name = "facet-kdl" version = "0.42.0" -source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a3459c7f26a83a45199b73d48f2708fa482b5d50bee30bd88a8f90f0765322b" dependencies = [ "facet", "facet-core", @@ -607,7 +611,8 @@ dependencies = [ [[package]] name = "facet-macro-parse" version = "0.42.0" -source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65b6c80c8dd3925eb08d246ffa6c7c9ccb2f1bc2a9dbe0cccea6c33a0f62cf66" dependencies = [ "facet-macro-types", "proc-macro2", @@ -617,7 +622,8 @@ dependencies = [ [[package]] name = "facet-macro-types" version = "0.42.0" -source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c6cacc78f28f5508765bc9625859ca34551bb62e3b9f9e172d325b13490b77" dependencies = [ "proc-macro2", "quote", @@ -627,7 +633,8 @@ dependencies = [ [[package]] name = "facet-macros" version = "0.42.0" -source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26a4aed1abc3de7cccbf7a162ae64b4aa3d3d1f0a5136c140f83c87b934a9775" dependencies = [ "facet-macros-impl", ] @@ -635,7 +642,8 @@ dependencies = [ [[package]] name = "facet-macros-impl" version = "0.42.0" -source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5e1e176fdc8615d7a023e1b1c4c05db5cb1dc5764add5ca1a5efc8f8b2ccb1d" dependencies = [ "facet-macro-parse", "facet-macro-types", @@ -648,7 +656,8 @@ dependencies = [ [[package]] name = "facet-path" version = "0.42.0" -source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd02b5b9608066f2417238aae3181a18a3b16b698aca7b448a2ec31ad0dba15c" dependencies = [ "arborium", "facet-core", @@ -660,7 +669,8 @@ dependencies = [ [[package]] name = "facet-pretty" version = "0.42.0" -source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "627fb7758b0f1973507d696f2844c28a84e1be6fc75dd6c734c05b33e07382f6" dependencies = [ "facet-core", "facet-reflect", @@ -670,7 +680,8 @@ dependencies = [ [[package]] name = "facet-reflect" version = "0.42.0" -source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8571d80d292b6a256d2c26a6e85dbb4bc7adad7768457c5157b1d9ab044ee24f" dependencies = [ "facet-core", "miette", @@ -679,12 +690,14 @@ dependencies = [ [[package]] name = "facet-singularize" version = "0.42.0" -source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f7d2db11f545897d8cabedd06b233055784dbb74b1e07259a325dec19b3979" [[package]] name = "facet-solver" version = "0.42.0" -source = "git+https://github.com/facet-rs/facet.git#1d81d9f53c9aa5ccd9370d4291ff5892795241f3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e7f3e9d1169c905d07e354cd1d8e4621945f1c1e55881332d30791781c0e52" dependencies = [ "facet-core", "facet-reflect", @@ -709,9 +722,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" [[package]] name = "fluent-uri" @@ -1174,9 +1187,9 @@ checksum = "7a46645bbd70538861a90d0f26c31537cdf1e44aae99a794fb75a664b70951bc" [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -1298,6 +1311,7 @@ dependencies = [ "super-table", "tempfile", "test-log", + "thiserror 2.0.17", "tokio", "tracing", "tracing-error", @@ -1312,9 +1326,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.179" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libredox" @@ -2158,9 +2172,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" -version = "2.0.113" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -3006,18 +3020,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 06cb549..bdfbe57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,8 @@ maintainer = "Dougefresh " section = "utility" priority = "optional" assets = [ - ["target/release/kg", "usr/bin/", "755"], - { source = "README.md", dest = "usr/share/doc/kiro-generator/README", mode = "644" }, + ["target/release/kg", "usr/bin/", "755"], + { source = "README.md", dest = "usr/share/doc/kiro-generator/README", mode = "644" }, ] [[bin]] @@ -30,12 +30,14 @@ colored = "3.0.0" dirs = "6" emojis-rs = "0.1.3" enum-iterator = "2.3.0" -facet = { git = "https://github.com/facet-rs/facet.git", features = [ - "auto-traits", - "reflect", - "simd" -] } -facet-kdl = { git = "https://github.com/facet-rs/facet.git" } +# facet = { git = "https://github.com/facet-rs/facet.git", features = [ +# "auto-traits", +# "reflect", +# "simd" +# ] } +facet = { version = "0.42.0", features = ["auto-traits", "reflect", "simd"] } +facet-kdl = { version = "0.42.0" } +# facet-kdl = { git = "https://github.com/facet-rs/facet.git" } futures = "0.3" indoc = "2.0.7" jsonschema = { version = "0.37", default-features = false, features = [ @@ -48,6 +50,7 @@ serde_json = { version = "1" } serde_yaml2 = "0.1.3" super-table = { version = "1", features = ["custom_styling"] } tempfile = "3" +thiserror = "2.0.17" tokio = { version = "1", features = ["fs", "macros", "rt-multi-thread"] } tracing = { version = "0.1" } tracing-error = "0.2.1" diff --git a/src/agent/mcp_config.rs b/src/agent/mcp_config.rs deleted file mode 100644 index 07e712b..0000000 --- a/src/agent/mcp_config.rs +++ /dev/null @@ -1,39 +0,0 @@ -use { - super::custom_tool::CustomToolConfig, - serde::{Deserialize, Serialize}, - std::collections::HashMap, -}; - -#[derive(Clone, Serialize, Deserialize, Debug, Default, Eq, PartialEq)] -#[serde(rename_all = "camelCase", transparent)] -pub struct McpServerConfig { - pub mcp_servers: HashMap, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn mcp_server_config_default() { - let config = McpServerConfig::default(); - assert!(config.mcp_servers.is_empty()); - } - - #[test] - fn mcp_server_config_serde() { - let mut config = McpServerConfig::default(); - config.mcp_servers.insert("test".into(), CustomToolConfig { - url: String::new(), - headers: HashMap::new(), - command: "cmd".into(), - args: vec![], - env: HashMap::new(), - timeout: 120_000, - disabled: false, - }); - let json = serde_json::to_string(&config).unwrap(); - let deserialized: McpServerConfig = serde_json::from_str(&json).unwrap(); - assert_eq!(config, deserialized); - } -} diff --git a/src/agent/mod.rs b/src/agent/mod.rs index d5e1c3f..204f698 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -1,6 +1,5 @@ mod custom_tool; pub mod hook; -mod mcp_config; pub mod tools; pub const DEFAULT_AGENT_RESOURCES: &[&str] = &["file://README.md", "file://AGENTS.md"]; pub const DEFAULT_APPROVE: [&str; 0] = []; @@ -16,7 +15,6 @@ use { }; pub use { custom_tool::{CustomToolConfig, tool_default_timeout}, - mcp_config::McpServerConfig, tools::*, }; @@ -37,7 +35,7 @@ pub struct Agent { pub prompt: Option, /// Configuration for Model Context Protocol (MCP) servers #[serde(default)] - pub mcp_servers: McpServerConfig, + pub mcp_servers: HashMap, /// List of tools the agent can see. Use \"@{MCP_SERVER_NAME}/tool_name\" to /// specify tools from mcp servers. To include all tools from a server, /// use \"@{MCP_SERVER_NAME}\" @@ -170,9 +168,7 @@ impl TryFrom<&KdlAgent> for Agent { name: value.name.clone(), description: value.description.clone(), prompt: value.prompt.clone(), - mcp_servers: McpServerConfig { - mcp_servers: value.mcp.clone(), - }, + mcp_servers: value.mcp.clone(), tools: if tools.is_empty() { default_agent.tools } else { diff --git a/src/config.rs b/src/config.rs index f3bd682..2ae86b8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,7 +10,7 @@ use { crate::Fs, facet::Facet, facet_kdl as kdl, - miette::{GraphicalReportHandler, GraphicalTheme, IntoDiagnostic}, + miette::IntoDiagnostic, std::{ collections::{HashMap, HashSet}, fmt::{Debug, Display}, @@ -58,14 +58,6 @@ impl AsRef> for GenericSet { } } -// #[cfg(test)] -// impl GenericSet { -// #[cfg(test)] -// fn len(&self) -> usize { -// self.item.len() -// } -// } - #[derive(Facet, Debug, Default, PartialEq, Clone, Eq)] #[facet(default)] pub(super) struct GenericVec { @@ -94,6 +86,7 @@ impl From for HashMap { } } +#[cfg(test)] impl GenericVec { fn len(&self) -> usize { self.item.len() @@ -117,7 +110,7 @@ fn print_error(e: &facet_kdl::KdlDeserializeError) { // let d = e.into_diagnostics(); eprintln!("\n=== Miette render ==="); let mut output = String::new(); - let handler = GraphicalReportHandler::new_themed(GraphicalTheme::unicode()); + let handler = miette::GraphicalReportHandler::new_themed(miette::GraphicalTheme::unicode()); handler.render_report(&mut output, e).unwrap(); eprintln!("{}", output); } @@ -138,7 +131,15 @@ where if fs.exists(&path) { match fs.read_to_string_sync(&path).into_diagnostic() { Err(e) => Some(Err(e)), - Ok(content) => Some(kdl::from_str(&content).into_diagnostic()), + Ok(content) => match kdl::from_str::(&content) { + Err(e) => { + let kdl_err = + &crate::Error::DeserializeError(path.as_ref().display().to_string(), e); + crate::output::print_error(kdl_err); + Some(Err(crate::format_err!("{kdl_err}"))) + } + Ok(r) => Some(Ok(r)), + }, } } else { None @@ -245,7 +246,7 @@ mod tests { native-tool { write { - allow "./src/*" + allow "./src/*" allow "./scripts/**" deny "Cargo.lock" override "/tmp" diff --git a/src/config/agent_file.rs b/src/config/agent_file.rs index ae421e9..b8deb5e 100644 --- a/src/config/agent_file.rs +++ b/src/config/agent_file.rs @@ -37,7 +37,7 @@ pub struct KdlAgentFileDoc { #[facet(kdl::children, default)] pub(super) resources: Vec, - #[facet(kdl::property, default)] + #[facet(kdl::child, default)] pub include_mcp_json: Option, #[facet(kdl::child, rename = "tools", default)] diff --git a/src/config/merge.rs b/src/config/merge.rs index b1d351d..54bb4f2 100644 --- a/src/config/merge.rs +++ b/src/config/merge.rs @@ -39,9 +39,8 @@ mod tests { description "I am a child" resource "file://child.md" resource "file://README.md" - inherit "parent" - tool "@awsdocs" - tool "shell" + inherits "parent" + tools "@awsdocs" "shell" native-tool { write { overrides "Cargo.lock" @@ -63,11 +62,10 @@ mod tests { description "I am parent" resource "file://parent.md" resource "file://README.md" - tool "web_search" - tool "shell" + tools "web_search" "shell" prompt "i tell you what to do" model "claude" - allowed-tool "write" + allowed-tools "write" alias "execute_bash" "shell" alias "fs_read" "read" native-tool { @@ -79,7 +77,7 @@ mod tests { allows "./src/*" "./scripts/**" denies "Cargo.lock" } - + shell { allows "git status .*" "git pull .*" denies "git push .*" diff --git a/src/config/native.rs b/src/config/native.rs index 0fd281c..bde8881 100644 --- a/src/config/native.rs +++ b/src/config/native.rs @@ -24,9 +24,30 @@ macro_rules! define_tool { impl $name { pub fn merge(mut self, other: Self) -> Self { - self.allows.extend(other.allows); - self.denies.extend(other.denies); - self.overrides.extend(other.overrides); + if !other.allows.is_empty() { + tracing::trace!( + tool = stringify!($name), + count = other.allows.len(), + "merging allows" + ); + self.allows.extend(other.allows); + } + if !other.denies.is_empty() { + tracing::trace!( + tool = stringify!($name), + count = other.denies.len(), + "merging denies" + ); + self.denies.extend(other.denies); + } + if !other.overrides.is_empty() { + tracing::trace!( + tool = stringify!($name), + count = other.overrides.len(), + "merging overrides" + ); + self.overrides.extend(other.overrides); + } self.disable_auto_readonly = self.disable_auto_readonly.or(other.disable_auto_readonly); self.deny_by_default = self.deny_by_default.or(other.deny_by_default); @@ -116,28 +137,6 @@ impl From for NativeTools { } } -#[derive(Facet, Debug, Clone, Default, PartialEq, Eq)] -pub struct GenericList { - #[facet(kdl::arguments)] - pub list: Vec, -} - -impl From<&'static str> for GenericList { - fn from(value: &'static str) -> Self { - Self { - list: vec![value.to_string()], - } - } -} - -impl FromIterator<&'static str> for GenericList { - fn from_iter>(iter: T) -> Self { - Self { - list: iter.into_iter().map(|f| f.to_string()).collect(), - } - } -} - impl NativeTools { pub fn merge(mut self, other: Self) -> Self { self.shell = self.shell.merge(other.shell); diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..1e7a86b --- /dev/null +++ b/src/error.rs @@ -0,0 +1,5 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("syntax error in file {0}")] + DeserializeError(String, facet_kdl::KdlDeserializeError), +} diff --git a/src/generator/merge.rs b/src/generator/merge.rs index 526a383..0828e5d 100644 --- a/src/generator/merge.rs +++ b/src/generator/merge.rs @@ -70,89 +70,91 @@ impl Generator { } } -// #[cfg(test)] -// mod tests { -// use {super::*, serde_yaml2::to_string}; - -// #[tokio::test] -// #[test_log::test] -// async fn test_merge_inheritance_chain() -> Result<()> { -// let fs = Fs::new(); -// let generator = Generator::new( -// fs, -// ConfigLocation::Local, -// crate::output::OutputFormat::Table(true), -// )?; - -// let merged = generator.merge()?; -// assert_eq!(merged.len(), 3); - -// // Find dependabot agent -// let dependabot = merged -// .iter() -// .find(|a| a.name == "dependabot") -// .expect("dependabot agent not found"); - -// // Verify inheritance chain was resolved: dependabot -> aws-test -> -// base assert_eq!(dependabot.description(), "asds"); - -// // Should have prompt from aws-test -// assert_eq!(dependabot.prompt(), "you are an AWS expert".to_string()); - -// // Should have tools from base -// let tools = dependabot.tools(); -// assert!(tools.contains("*")); - -// // Should have allowed_tools merged from base and aws-test -// let allowed = dependabot.allowed_tools(); -// assert!(allowed.contains("read")); -// assert!(allowed.contains("knowledge")); -// assert!(allowed.contains("@fetch")); -// assert!(allowed.contains("@awsdocs")); - -// // Should have resources from all three -// let resources: Vec = dependabot.resources().map(|s| -// s.to_string()).collect(); assert!(resources.contains(&"file://README.md".to_string())); -// assert!(resources.contains(&"file://AGENTS.md".to_string())); -// assert!(resources.contains(&"file://.amazonq/rules/**/*.md".to_string())); - -// // Should have hooks from all levels -// let hooks = dependabot.hooks(); -// assert!(hooks.contains_key(& -// crate::agent::hook::HookTrigger::AgentSpawn)); - -// // Should have force permissions from dependabot overriding denies -// from base let shell = dependabot.get_tool_shell(); -// let overrides = shell.override_commands(); -// assert!(overrides.contains(&"git commit .*".into())); -// assert!(overrides.contains(&"git push .*".into())); - -// let read = dependabot.get_tool_read(); -// let overrides = read.override_paths(); -// assert!(overrides.contains(&".*Cargo.toml.*".into())); - -// let write = dependabot.get_tool_write(); -// let overrides = read.override_paths(); -// assert!(overrides.contains(&".*Cargo.toml.*".into())); - -// // Should have aws tool from aws-test -// let aws = dependabot.get_tool_aws(); - -// assert!( -// aws.allow -// .unwrap_or_default() -// .list -// .iter() -// .any(|i| i == "ec2") -// ); -// assert!(aws.allow.unwrap_or_default().list.iter().any(|i| i == -// "s3")); assert!(aws.deny.unwrap_or_default().list.iter().any(|i| i == -// "iam")); - -// // check try_from -// let results = generator.write_all(true).await?; -// assert!(!results.is_empty()); - -// Ok(()) -// } -// } +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + #[test_log::test] + async fn test_merge_inheritance_chain() -> Result<()> { + let fs = Fs::new(); + let generator = Generator::new( + fs, + ConfigLocation::Local, + crate::output::OutputFormat::Table(true), + )?; + + let merged = generator.merge()?; + assert_eq!(merged.len(), 3); + + // Find dependabot agent + let dependabot = merged + .iter() + .find(|a| a.name == "dependabot") + .expect("dependabot agent not found"); + + // Verify inheritance chain was resolved: dependabot -> aws-test -> base + assert_eq!( + dependabot.description.clone().unwrap_or_default(), + "I make life painful for developers" + ); + + // Should have prompt from aws-test + assert_eq!( + dependabot.prompt.clone().unwrap_or_default(), + "you are an AWS expert".to_string() + ); + + // Should have tools from base + let tools = &dependabot.tools; + assert!(tools.contains("*")); + + // Should have allowed_tools merged from base and aws-test + let allowed = &dependabot.allowed_tools; + assert!(allowed.contains("read")); + assert!(allowed.contains("knowledge")); + assert!(allowed.contains("fetch")); + assert!(allowed.contains("@awsdocs")); + + // Should have resources from all three + let resources = &dependabot.resources; + assert!(resources.contains(&"file://README.md".to_string())); + assert!(resources.contains(&"file://AGENTS.md".to_string())); + assert!(resources.contains(&"file://.amazonq/rules/**/*.md".to_string())); + + // Should have hooks from all levels + let hooks = &dependabot.hook; + assert!( + !hooks + .hooks(&crate::agent::hook::HookTrigger::AgentSpawn) + .is_empty() + ); + + // Should have force permissions from dependabot overriding denies from base + let shell = dependabot.get_tool_shell(); + let overrides = &shell.overrides; + assert!(overrides.contains("git commit .*")); + assert!(overrides.contains("git push .*")); + + let read = dependabot.get_tool_read(); + let overrides = &read.overrides; + assert!(overrides.contains(".*Cargo.toml.*")); + + let write = dependabot.get_tool_write(); + let overrides = &write.overrides; + assert!(overrides.contains(".*Cargo.toml.*")); + + // Should have aws tool from aws-test + let aws = dependabot.get_tool_aws(); + + assert_eq!(2, aws.allows.len()); + assert!(aws.allows.contains("ec2")); + assert!(aws.allows.contains("s3")); + + // check try_from + let results = generator.write_all(true).await?; + assert!(!results.is_empty()); + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 54ed3d2..63e9c7d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod agent; mod commands; mod config; +mod error; mod generator; // mod kdl; mod os; @@ -8,7 +9,6 @@ pub mod output; mod schema; mod source; -pub use miette::miette as format_err; use { crate::{generator::Generator, os::Fs}, clap::Parser, @@ -18,6 +18,7 @@ use { tracing_error::ErrorLayer, tracing_subscriber::prelude::*, }; +pub use {error::Error, miette::miette as format_err}; pub type Result = miette::Result; pub(crate) const DOCS_URL: &str = "https://kg.cartera-mesh.com"; diff --git a/src/output.rs b/src/output.rs index 6f27fd9..e2f5b1c 100644 --- a/src/output.rs +++ b/src/output.rs @@ -12,11 +12,15 @@ use { tracing::enabled, }; -pub fn print_error(color: &ColorOverride, e: &facet_kdl::KdlDeserializeError) { - let mut output = String::new(); - let handler = GraphicalReportHandler::new_themed(GraphicalTheme::unicode()); - handler.render_report(&mut output, e).unwrap(); - eprintln!("{}", output); +pub fn print_error(e: &crate::Error) { + match e { + crate::Error::DeserializeError(file, kdl_err) => { + let mut output = String::new(); + let handler = GraphicalReportHandler::new_themed(GraphicalTheme::unicode()); + handler.render_report(&mut output, kdl_err).unwrap(); + eprintln!("{}\nFile location: '{}'", output, file); + } + }; } /// Override the color setting. Default is [`ColorOverride::Auto`]. From 8a597e8513ed3b4c9d47f31e5ef60295a9203831 Mon Sep 17 00:00:00 2001 From: dougeEfresh Date: Fri, 9 Jan 2026 12:34:26 +0000 Subject: [PATCH 8/8] feat: agent test --- src/agent/mod.rs | 20 ++++++++++++++++++++ src/config.rs | 43 +------------------------------------------ src/config/agent.rs | 19 ------------------- 3 files changed, 21 insertions(+), 61 deletions(-) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 204f698..72b472f 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -225,3 +225,23 @@ impl Default for Agent { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_agent() -> crate::Result<()> { + let agent = Agent { + name: "test".to_string(), + ..Default::default() + }; + assert_eq!("test", format!("{agent}")); + + let kg_agent = KdlAgent::default(); + let agent = Agent::try_from(&kg_agent)?; + assert_eq!(agent.tools, Agent::default().tools); + + Ok(()) + } +} diff --git a/src/config.rs b/src/config.rs index 2ae86b8..36d777c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,7 +13,7 @@ use { miette::IntoDiagnostic, std::{ collections::{HashMap, HashSet}, - fmt::{Debug, Display}, + fmt::Debug, path::Path, }, }; @@ -27,18 +27,6 @@ pub(super) struct GenericItem { pub item: String, } -impl Display for GenericItem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.item) - } -} - -impl AsRef for GenericItem { - fn as_ref(&self) -> &str { - self.item.as_ref() - } -} - #[derive(Facet, Debug, Default, PartialEq, Clone, Eq)] #[facet(default)] pub(super) struct GenericSet { @@ -46,18 +34,6 @@ pub(super) struct GenericSet { pub item: HashSet, } -impl Display for GenericSet { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.item) - } -} - -impl AsRef> for GenericSet { - fn as_ref(&self) -> &HashSet { - &self.item - } -} - #[derive(Facet, Debug, Default, PartialEq, Clone, Eq)] #[facet(default)] pub(super) struct GenericVec { @@ -65,18 +41,6 @@ pub(super) struct GenericVec { pub item: Vec, } -impl Display for GenericVec { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.item) - } -} - -impl AsRef> for GenericVec { - fn as_ref(&self) -> &Vec { - &self.item - } -} - impl From for HashMap { fn from(list: GenericVec) -> HashMap { list.item @@ -99,11 +63,6 @@ pub(super) struct IntDoc { #[facet(kdl::argument)] pub value: u64, } -impl AsRef for IntDoc { - fn as_ref(&self) -> &u64 { - &self.value - } -} #[cfg(test)] fn print_error(e: &facet_kdl::KdlDeserializeError) { diff --git a/src/config/agent.rs b/src/config/agent.rs index 37f5409..fa3fc05 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -193,25 +193,6 @@ impl KdlAgent { } impl KdlAgentDoc { - pub fn prompt(&self) -> String { - self.prompt.clone().unwrap_or_default() - } - - pub fn description(&self) -> String { - self.description.clone().unwrap_or_default() - } - - pub fn new(name: impl AsRef) -> Self { - Self { - name: name.as_ref().to_string(), - ..Default::default() - } - } - - pub fn is_template(&self) -> bool { - self.template.is_some_and(|f| f) - } - pub fn tool_aliases(&self) -> HashMap { let mut map: HashMap = HashMap::new(); for a in &self.alias {