diff --git a/Cargo.lock b/Cargo.lock index 73689e162..da0155857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,6 +59,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -105,9 +106,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -120,15 +121,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -463,6 +464,21 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -487,6 +503,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" @@ -557,6 +585,30 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bson" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969a9ba84b0ff843813e7249eed1678d9b6607ce5a3b8f0a47af3fcf7978e6e" +dependencies = [ + "ahash", + "base64 0.22.1", + "bitvec", + "chrono", + "getrandom 0.2.17", + "getrandom 0.3.4", + "hex", + "indexmap 2.13.0", + "js-sys", + "once_cell", + "rand 0.9.2", + "serde", + "serde_bytes", + "serde_json", + "time", + "uuid", +] + [[package]] name = "bufstream" version = "0.1.4" @@ -692,9 +744,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -797,9 +849,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -807,9 +859,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -819,18 +871,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.66" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031" +checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -840,9 +892,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cobs" @@ -855,9 +907,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -925,12 +977,41 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.18.1" @@ -1146,6 +1227,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "cron" version = "0.15.0" @@ -1238,6 +1325,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -1266,6 +1359,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + [[package]] name = "cssparser-macros" version = "0.6.1" @@ -1322,38 +1428,14 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.117", + "darling_core", + "darling_macro", ] [[package]] @@ -1369,24 +1451,13 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.117", -] - [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.117", ] @@ -1440,6 +1511,28 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -1457,13 +1550,36 @@ version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", "syn 2.0.117", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", + "unicode-xid", +] + [[package]] name = "digest" version = "0.10.7" @@ -1583,6 +1699,21 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dom_query" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d9c2e7f1d22d0f2ce07626d259b8a55f4a47cb0938d4006dd8ae037f17d585e" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.36.1", + "precomputed-hash", + "selectors 0.35.0", + "tendril", +] + [[package]] name = "dpi" version = "0.1.2" @@ -1713,6 +1844,18 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -1870,6 +2013,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1921,6 +2070,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -2465,7 +2620,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", "serde", ] @@ -2508,6 +2663,52 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.2", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.2", + "resolv-conf", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2545,10 +2746,20 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.14.1", "match_token", ] +[[package]] +name = "html5ever" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6452c4751a24e1b99c3260d505eaeee76a050573e61f30ac2c924ddc7236f01e" +dependencies = [ + "log", + "markup5ever 0.36.1", +] + [[package]] name = "http" version = "1.4.0" @@ -2820,9 +3031,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -2913,13 +3124,25 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" dependencies = [ - "darling 0.23.0", + "darling", "indoc", "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg 0.50.0", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -3120,10 +3343,10 @@ version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ - "cssparser", - "html5ever", + "cssparser 0.29.6", + "html5ever 0.29.1", "indexmap 2.13.0", - "selectors", + "selectors 0.24.0", ] [[package]] @@ -3305,9 +3528,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mac-notification-sys" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c889829df2867fd6a043c5932c641fcf7fe9d4329317326af08df14747ab9a97" +checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3" dependencies = [ "cc", "objc2", @@ -3324,6 +3547,54 @@ dependencies = [ "libc", ] +[[package]] +name = "macro_magic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc33f9f0351468d26fbc53d9ce00a096c8522ecb42f19b50f34f2c422f76d21d" +dependencies = [ + "macro_magic_core", + "macro_magic_macros", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "macro_magic_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1687dc887e42f352865a393acae7cf79d98fab6351cde1f58e9e057da89bf150" +dependencies = [ + "const-random", + "derive-syn-parse", + "macro_magic_core_macros", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "macro_magic_core_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "macro_magic_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" +dependencies = [ + "macro_magic_core", + "quote", + "syn 2.0.117", +] + [[package]] name = "mailparse" version = "0.16.1" @@ -3344,11 +3615,22 @@ dependencies = [ "log", "phf 0.11.3", "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", "tendril", ] +[[package]] +name = "markup5ever" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c3294c4d74d0742910f8c7b466f44dda9eb2d5742c1e430138df290a1e8451c" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + [[package]] name = "match_token" version = "0.1.0" @@ -3381,6 +3663,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.8.0" @@ -3438,22 +3730,115 @@ dependencies = [ ] [[package]] -name = "mio" -version = "1.1.1" +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "mongocrypt" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da0cd419a51a5fb44819e290fbdb0665a54f21dead8923446a799c7f4d26ad9" +dependencies = [ + "bson", + "mongocrypt-sys", + "once_cell", + "serde", +] + +[[package]] +name = "mongocrypt-sys" +version = "0.1.5+1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224484c5d09285a7b8cb0a0c117e847ebd14cb6e4470ecf68cdb89c503b0edb9" + +[[package]] +name = "mongodb" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803dd859e8afa084c255a8effd8000ff86f7c8076a50cd6d8c99e8f3496f75c2" +dependencies = [ + "base64 0.22.1", + "bitflags 2.11.0", + "bson", + "derive-where", + "derive_more 2.1.1", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hickory-proto", + "hickory-resolver", + "hmac", + "macro_magic", + "md-5", + "mongocrypt", + "mongodb-internal-macros", + "pbkdf2", + "percent-encoding", + "rand 0.9.2", + "rustc_version_runtime", + "rustls", + "rustversion", + "serde", + "serde_bytes", + "serde_with", + "sha1", + "sha2", + "socket2 0.6.3", + "stringprep", + "strsim", + "take_mut", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tokio-util", + "typed-builder", + "uuid", + "webpki-roots", +] + +[[package]] +name = "mongodb-internal-macros" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "a973ef3dd3dbc6f6e65bbdecfd9ec5e781b9e7493b0f369a7c62e35d8e5ae2c8" dependencies = [ - "libc", - "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.61.2", + "macro_magic", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] name = "moxcms" -version = "0.7.11" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -3596,9 +3981,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-traits" @@ -3781,9 +4166,13 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -4027,7 +4416,10 @@ name = "openfang-memory" version = "0.4.5" dependencies = [ "async-trait", + "bson", "chrono", + "futures", + "mongodb", "openfang-types", "rmp-serde", "rusqlite", @@ -4067,11 +4459,13 @@ dependencies = [ "anyhow", "async-trait", "base64 0.22.1", + "bson", "bytes", "chrono", "dashmap", "futures", "hex", + "mongodb", "openfang-memory", "openfang-skills", "openfang-types", @@ -4160,9 +4554,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags 2.11.0", "cfg-if", @@ -4192,9 +4586,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -4319,6 +4713,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -4417,6 +4820,17 @@ dependencies = [ "phf_shared 0.12.1", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -4437,6 +4851,16 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -4467,6 +4891,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + [[package]] name = "phf_macros" version = "0.10.0" @@ -4494,6 +4928,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -4530,6 +4977,15 @@ dependencies = [ "siphasher 1.0.2", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -4920,6 +5376,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -5286,6 +5748,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "rfd" version = "0.16.0" @@ -5385,6 +5853,16 @@ dependencies = [ "semver", ] +[[package]] +name = "rustc_version_runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" +dependencies = [ + "rustc_version", + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -5509,9 +5987,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -5603,14 +6081,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", - "cssparser", - "derive_more", + "cssparser 0.29.6", + "derive_more 0.99.20", "fxhash", "log", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", - "servo_arc", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fdfed56cd634f04fe8b9ddf947ae3dc493483e819593d2ba17df9ad05db8b2" +dependencies = [ + "bitflags 2.11.0", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", "smallvec", ] @@ -5646,6 +6143,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -5683,6 +6190,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ + "indexmap 2.13.0", "itoa", "memchr", "serde", @@ -5744,9 +6252,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", @@ -5763,11 +6271,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling 0.21.3", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -5818,6 +6326,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha1" version = "0.10.6" @@ -6091,6 +6608,18 @@ dependencies = [ "serde", ] +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + [[package]] name = "string_cache_codegen" version = "0.5.4" @@ -6103,6 +6632,29 @@ dependencies = [ "quote", ] +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -6203,6 +6755,18 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + [[package]] name = "tao" version = "0.34.6" @@ -6252,6 +6816,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.44" @@ -6627,7 +7197,7 @@ dependencies = [ "ctor", "dunce", "glob", - "html5ever", + "html5ever 0.29.1", "http", "infer", "json-patch", @@ -6678,9 +7248,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -6760,9 +7330,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -6775,20 +7345,29 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -6801,9 +7380,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -6910,6 +7489,7 @@ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -7128,9 +7708,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -7212,6 +7792,26 @@ dependencies = [ "utf-8", ] +[[package]] +name = "typed-builder" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "typeid" version = "1.0.3" @@ -7232,9 +7832,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uds_windows" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", @@ -7288,12 +7888,33 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -8003,6 +8624,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + [[package]] name = "webkit2gtk" version = "2.0.2" @@ -8101,6 +8734,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -8315,6 +8954,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -8366,6 +9014,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -8423,6 +9086,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -8441,6 +9110,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -8459,6 +9134,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -8489,6 +9170,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -8507,6 +9194,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -8525,6 +9218,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -8543,6 +9242,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -8591,6 +9296,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "winreg" version = "0.55.0" @@ -8715,24 +9430,23 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wry" -version = "0.54.2" +version = "0.54.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a" +checksum = "a24eda84b5d488f99344e54b807138896cee8df0b2d16c793f1f6b80e6d8df1f" dependencies = [ "base64 0.22.1", "block2", "cookie", "crossbeam-channel", "dirs 6.0.0", + "dom_query", "dpi", "dunce", "gdkx11", "gtk", - "html5ever", "http", "javascriptcore-rs", "jni", - "kuchikiki", "libc", "ndk", "objc2", @@ -8758,6 +9472,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.21.0" @@ -8902,18 +9625,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index e68974f59..d3c7cd49d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,8 @@ uuid = { version = "1", features = ["v4", "v5", "serde"] } # Database rusqlite = { version = "0.31", features = ["bundled", "serde_json"] } +mongodb = { version = "3" } +bson = { version = "2", features = ["chrono-0_4"] } # CLI clap = { version = "4", features = ["derive"] } diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index 456d867f0..225f1ecbd 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -5169,7 +5169,8 @@ pub async fn usage_stats(State(state): State>) -> impl IntoRespons /// GET /api/usage/summary — Get overall usage summary from UsageStore. pub async fn usage_summary(State(state): State>) -> impl IntoResponse { - match state.kernel.memory.usage().query_summary(None) { + let usage_store = state.kernel.memory.create_usage_store(); + match usage_store.query_summary(None) { Ok(s) => Json(serde_json::json!({ "total_input_tokens": s.total_input_tokens, "total_output_tokens": s.total_output_tokens, @@ -5189,7 +5190,8 @@ pub async fn usage_summary(State(state): State>) -> impl IntoRespo /// GET /api/usage/by-model — Get usage grouped by model. pub async fn usage_by_model(State(state): State>) -> impl IntoResponse { - match state.kernel.memory.usage().query_by_model() { + let usage_store = state.kernel.memory.create_usage_store(); + match usage_store.query_by_model() { Ok(models) => { let list: Vec = models .iter() @@ -5211,9 +5213,10 @@ pub async fn usage_by_model(State(state): State>) -> impl IntoResp /// GET /api/usage/daily — Get daily usage breakdown for the last 7 days. pub async fn usage_daily(State(state): State>) -> impl IntoResponse { - let days = state.kernel.memory.usage().query_daily_breakdown(7); - let today_cost = state.kernel.memory.usage().query_today_cost(); - let first_event = state.kernel.memory.usage().query_first_event_date(); + let usage_store = state.kernel.memory.create_usage_store(); + let days = usage_store.query_daily_breakdown(7); + let today_cost = usage_store.query_today_cost(); + let first_event = usage_store.query_first_event_date(); let days_list = match days { Ok(d) => d @@ -5312,7 +5315,7 @@ pub async fn agent_budget_status( }; let quota = &entry.manifest.resources; - let usage_store = openfang_memory::usage::UsageStore::new(state.kernel.memory.usage_conn()); + let usage_store = state.kernel.memory.create_usage_store(); let hourly = usage_store.query_hourly(agent_id).unwrap_or(0.0); let daily = usage_store.query_daily(agent_id).unwrap_or(0.0); let monthly = usage_store.query_monthly(agent_id).unwrap_or(0.0); @@ -5352,7 +5355,7 @@ pub async fn agent_budget_status( /// GET /api/budget/agents — Per-agent cost ranking (top spenders). pub async fn agent_budget_ranking(State(state): State>) -> impl IntoResponse { - let usage_store = openfang_memory::usage::UsageStore::new(state.kernel.memory.usage_conn()); + let usage_store = state.kernel.memory.create_usage_store(); let agents: Vec = state .kernel .registry diff --git a/crates/openfang-desktop/gen/schemas/macOS-schema.json b/crates/openfang-desktop/gen/schemas/macOS-schema.json new file mode 100644 index 000000000..c1dc14d8c --- /dev/null +++ b/crates/openfang-desktop/gen/schemas/macOS-schema.json @@ -0,0 +1,2990 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + }, + "deny": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + } + } + }, + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n\n#### This default permission set includes:\n\n- `allow-enable`\n- `allow-disable`\n- `allow-is-enabled`", + "type": "string", + "const": "autostart:default", + "markdownDescription": "This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n\n#### This default permission set includes:\n\n- `allow-enable`\n- `allow-disable`\n- `allow-is-enabled`" + }, + { + "description": "Enables the disable command without any pre-configured scope.", + "type": "string", + "const": "autostart:allow-disable", + "markdownDescription": "Enables the disable command without any pre-configured scope." + }, + { + "description": "Enables the enable command without any pre-configured scope.", + "type": "string", + "const": "autostart:allow-enable", + "markdownDescription": "Enables the enable command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "autostart:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the disable command without any pre-configured scope.", + "type": "string", + "const": "autostart:deny-disable", + "markdownDescription": "Denies the disable command without any pre-configured scope." + }, + { + "description": "Denies the enable command without any pre-configured scope.", + "type": "string", + "const": "autostart:deny-enable", + "markdownDescription": "Denies the enable command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "autostart:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope." + }, + { + "description": "Enables the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope." + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope." + }, + { + "description": "Denies the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope." + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, + { + "description": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n", + "type": "string", + "const": "global-shortcut:default", + "markdownDescription": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n" + }, + { + "description": "Enables the is_registered command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-is-registered", + "markdownDescription": "Enables the is_registered command without any pre-configured scope." + }, + { + "description": "Enables the register command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-register", + "markdownDescription": "Enables the register command without any pre-configured scope." + }, + { + "description": "Enables the register_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-register-all", + "markdownDescription": "Enables the register_all command without any pre-configured scope." + }, + { + "description": "Enables the unregister command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-unregister", + "markdownDescription": "Enables the unregister command without any pre-configured scope." + }, + { + "description": "Enables the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-unregister-all", + "markdownDescription": "Enables the unregister_all command without any pre-configured scope." + }, + { + "description": "Denies the is_registered command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-is-registered", + "markdownDescription": "Denies the is_registered command without any pre-configured scope." + }, + { + "description": "Denies the register command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-register", + "markdownDescription": "Denies the register command without any pre-configured scope." + }, + { + "description": "Denies the register_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-register-all", + "markdownDescription": "Denies the register_all command without any pre-configured scope." + }, + { + "description": "Denies the unregister command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-unregister", + "markdownDescription": "Denies the unregister command without any pre-configured scope." + }, + { + "description": "Denies the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-unregister-all", + "markdownDescription": "Denies the unregister_all command without any pre-configured scope." + }, + { + "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`", + "type": "string", + "const": "notification:default", + "markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`" + }, + { + "description": "Enables the batch command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-batch", + "markdownDescription": "Enables the batch command without any pre-configured scope." + }, + { + "description": "Enables the cancel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-cancel", + "markdownDescription": "Enables the cancel command without any pre-configured scope." + }, + { + "description": "Enables the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-check-permissions", + "markdownDescription": "Enables the check_permissions command without any pre-configured scope." + }, + { + "description": "Enables the create_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-create-channel", + "markdownDescription": "Enables the create_channel command without any pre-configured scope." + }, + { + "description": "Enables the delete_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-delete-channel", + "markdownDescription": "Enables the delete_channel command without any pre-configured scope." + }, + { + "description": "Enables the get_active command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-get-active", + "markdownDescription": "Enables the get_active command without any pre-configured scope." + }, + { + "description": "Enables the get_pending command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-get-pending", + "markdownDescription": "Enables the get_pending command without any pre-configured scope." + }, + { + "description": "Enables the is_permission_granted command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-is-permission-granted", + "markdownDescription": "Enables the is_permission_granted command without any pre-configured scope." + }, + { + "description": "Enables the list_channels command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-list-channels", + "markdownDescription": "Enables the list_channels command without any pre-configured scope." + }, + { + "description": "Enables the notify command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-notify", + "markdownDescription": "Enables the notify command without any pre-configured scope." + }, + { + "description": "Enables the permission_state command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-permission-state", + "markdownDescription": "Enables the permission_state command without any pre-configured scope." + }, + { + "description": "Enables the register_action_types command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-register-action-types", + "markdownDescription": "Enables the register_action_types command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_active command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-remove-active", + "markdownDescription": "Enables the remove_active command without any pre-configured scope." + }, + { + "description": "Enables the request_permission command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-request-permission", + "markdownDescription": "Enables the request_permission command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Denies the batch command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-batch", + "markdownDescription": "Denies the batch command without any pre-configured scope." + }, + { + "description": "Denies the cancel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-cancel", + "markdownDescription": "Denies the cancel command without any pre-configured scope." + }, + { + "description": "Denies the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-check-permissions", + "markdownDescription": "Denies the check_permissions command without any pre-configured scope." + }, + { + "description": "Denies the create_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-create-channel", + "markdownDescription": "Denies the create_channel command without any pre-configured scope." + }, + { + "description": "Denies the delete_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-delete-channel", + "markdownDescription": "Denies the delete_channel command without any pre-configured scope." + }, + { + "description": "Denies the get_active command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-get-active", + "markdownDescription": "Denies the get_active command without any pre-configured scope." + }, + { + "description": "Denies the get_pending command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-get-pending", + "markdownDescription": "Denies the get_pending command without any pre-configured scope." + }, + { + "description": "Denies the is_permission_granted command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-is-permission-granted", + "markdownDescription": "Denies the is_permission_granted command without any pre-configured scope." + }, + { + "description": "Denies the list_channels command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-list-channels", + "markdownDescription": "Denies the list_channels command without any pre-configured scope." + }, + { + "description": "Denies the notify command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-notify", + "markdownDescription": "Denies the notify command without any pre-configured scope." + }, + { + "description": "Denies the permission_state command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-permission-state", + "markdownDescription": "Denies the permission_state command without any pre-configured scope." + }, + { + "description": "Denies the register_action_types command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-register-action-types", + "markdownDescription": "Denies the register_action_types command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_active command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-remove-active", + "markdownDescription": "Denies the remove_active command without any pre-configured scope." + }, + { + "description": "Denies the request_permission command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-request-permission", + "markdownDescription": "Denies the request_permission command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + }, + { + "description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`", + "type": "string", + "const": "updater:default", + "markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`" + }, + { + "description": "Enables the check command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-check", + "markdownDescription": "Enables the check command without any pre-configured scope." + }, + { + "description": "Enables the download command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-download", + "markdownDescription": "Enables the download command without any pre-configured scope." + }, + { + "description": "Enables the download_and_install command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-download-and-install", + "markdownDescription": "Enables the download_and_install command without any pre-configured scope." + }, + { + "description": "Enables the install command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-install", + "markdownDescription": "Enables the install command without any pre-configured scope." + }, + { + "description": "Denies the check command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-check", + "markdownDescription": "Denies the check command without any pre-configured scope." + }, + { + "description": "Denies the download command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-download", + "markdownDescription": "Denies the download command without any pre-configured scope." + }, + { + "description": "Denies the download_and_install command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-download-and-install", + "markdownDescription": "Denies the download_and_install command without any pre-configured scope." + }, + { + "description": "Denies the install command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-install", + "markdownDescription": "Denies the install command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "ShellScopeEntryAllowedArg": { + "description": "A command argument allowed to be executed by the webview API.", + "anyOf": [ + { + "description": "A non-configurable argument that is passed to the command in the order it was specified.", + "type": "string" + }, + { + "description": "A variable that is set while calling the command from the webview API.", + "type": "object", + "required": [ + "validator" + ], + "properties": { + "raw": { + "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", + "default": false, + "type": "boolean" + }, + "validator": { + "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ", + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "ShellScopeEntryAllowedArgs": { + "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", + "anyOf": [ + { + "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", + "type": "boolean" + }, + { + "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", + "type": "array", + "items": { + "$ref": "#/definitions/ShellScopeEntryAllowedArg" + } + } + ] + } + } +} \ No newline at end of file diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 816132fba..bb9366032 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -555,16 +555,26 @@ impl OpenFangKernel { std::fs::create_dir_all(&config.data_dir) .map_err(|e| KernelError::BootFailed(format!("Failed to create data dir: {e}")))?; - // Initialize memory substrate - let db_path = config - .memory - .sqlite_path - .clone() - .unwrap_or_else(|| config.data_dir.join("openfang.db")); - let memory = Arc::new( - MemorySubstrate::open(&db_path, config.memory.decay_rate) - .map_err(|e| KernelError::BootFailed(format!("Memory init failed: {e}")))?, - ); + // Initialize memory substrate (SQLite or MongoDB) + let memory: Arc = Arc::new(match config.memory.backend.as_str() { + "mongodb" => { + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on( + MemorySubstrate::open_with_config(&config.memory), + ) + }) + .map_err(|e| KernelError::BootFailed(format!("MongoDB memory init failed: {e}")))? + } + _ => { + let db_path = config + .memory + .sqlite_path + .clone() + .unwrap_or_else(|| config.data_dir.join("openfang.db")); + MemorySubstrate::open(&db_path, config.memory.decay_rate) + .map_err(|e| KernelError::BootFailed(format!("Memory init failed: {e}")))? + } + }); // Initialize credential resolver (vault → dotenv → env var) let credential_resolver = { @@ -715,9 +725,9 @@ impl OpenFangKernel { Arc::new(StubDriver) as Arc }; - // Initialize metering engine (shares the same SQLite connection as the memory substrate) + // Initialize metering engine (shares the same DB connection as the memory substrate) let metering = Arc::new(MeteringEngine::new(Arc::new( - openfang_memory::usage::UsageStore::new(memory.usage_conn()), + memory.create_usage_store(), ))); let supervisor = Supervisor::new(); @@ -1010,7 +1020,10 @@ impl OpenFangKernel { workflows: WorkflowEngine::new(), triggers: TriggerEngine::new(), background, - audit_log: Arc::new(AuditLog::with_db(memory.usage_conn())), + audit_log: Arc::new(match memory.is_mongo() { + true => AuditLog::with_mongo_db(memory.mongo_db().unwrap()), + false => AuditLog::with_db(memory.usage_conn().unwrap()), + }), metering, default_driver: driver, wasm_sandbox, diff --git a/crates/openfang-kernel/src/metering.rs b/crates/openfang-kernel/src/metering.rs index 651de4292..5d46fbf7d 100644 --- a/crates/openfang-kernel/src/metering.rs +++ b/crates/openfang-kernel/src/metering.rs @@ -515,7 +515,7 @@ mod tests { fn setup() -> MeteringEngine { let substrate = MemorySubstrate::open_in_memory(0.1).unwrap(); - let store = Arc::new(UsageStore::new(substrate.usage_conn())); + let store = Arc::new(substrate.create_usage_store()); MeteringEngine::new(store) } diff --git a/crates/openfang-memory/Cargo.toml b/crates/openfang-memory/Cargo.toml index c33bf8406..72b05e16c 100644 --- a/crates/openfang-memory/Cargo.toml +++ b/crates/openfang-memory/Cargo.toml @@ -12,6 +12,9 @@ serde = { workspace = true } serde_json = { workspace = true } rmp-serde = { workspace = true } rusqlite = { workspace = true } +mongodb = { workspace = true } +bson = { workspace = true } +futures = { workspace = true } chrono = { workspace = true } uuid = { workspace = true } thiserror = { workspace = true } diff --git a/crates/openfang-memory/src/lib.rs b/crates/openfang-memory/src/lib.rs index 15a4fbd44..f2114b737 100644 --- a/crates/openfang-memory/src/lib.rs +++ b/crates/openfang-memory/src/lib.rs @@ -1,15 +1,19 @@ //! Memory substrate for the OpenFang Agent Operating System. //! -//! Provides a unified memory API over three storage backends: -//! - **Structured store** (SQLite): Key-value pairs, sessions, agent state -//! - **Semantic store**: Text-based search (Phase 1: LIKE matching, Phase 2: Qdrant vectors) -//! - **Knowledge graph** (SQLite): Entities and relations +//! Provides a unified memory API backed by either SQLite or MongoDB. +//! The backend is selected via `MemoryConfig::backend` ("sqlite" or "mongodb"). //! -//! Agents interact with a single `Memory` trait that abstracts over all three stores. +//! Storage layers: +//! - **Structured store**: Key-value pairs, sessions, agent state +//! - **Semantic store**: Text-based search with optional vector embeddings +//! - **Knowledge graph**: Entities and relations +//! +//! Agents interact with a single `Memory` trait that abstracts over all stores. pub mod consolidation; pub mod knowledge; pub mod migration; +pub mod mongo; pub mod semantic; pub mod session; pub mod structured; diff --git a/crates/openfang-memory/src/mongo/consolidation.rs b/crates/openfang-memory/src/mongo/consolidation.rs new file mode 100644 index 000000000..8aeb19086 --- /dev/null +++ b/crates/openfang-memory/src/mongo/consolidation.rs @@ -0,0 +1,63 @@ +//! MongoDB memory consolidation and decay logic. + +use chrono::Utc; +use bson::doc; +use mongodb::Collection; +use openfang_types::error::{OpenFangError, OpenFangResult}; +use openfang_types::memory::ConsolidationReport; + +/// Memory consolidation engine backed by MongoDB. +#[derive(Clone)] +pub struct MongoConsolidationEngine { + memories: Collection, + decay_rate: f32, +} + +impl MongoConsolidationEngine { + pub fn new(db: mongodb::Database, decay_rate: f32) -> Self { + Self { + memories: db.collection("memories"), + decay_rate, + } + } + + /// Run a consolidation cycle: decay old memories. + pub async fn consolidate(&self) -> OpenFangResult { + let start = std::time::Instant::now(); + + // Decay confidence of memories not accessed in the last 7 days + let cutoff = bson::DateTime::from_chrono( + Utc::now() - chrono::Duration::days(7), + ); + let decay_factor = 1.0 - self.decay_rate as f64; + + let filter = doc! { + "deleted": false, + "accessed_at": { "$lt": cutoff }, + "confidence": { "$gt": 0.1 }, + }; + + // Use an aggregation pipeline update to compute new confidence in-place + let update = vec![doc! { + "$set": { + "confidence": { + "$max": [0.1, { "$multiply": ["$confidence", decay_factor] }] + } + } + }]; + + let result = self + .memories + .update_many(filter, update) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let duration_ms = start.elapsed().as_millis() as u64; + + Ok(ConsolidationReport { + memories_merged: 0, + memories_decayed: result.modified_count, + duration_ms, + }) + } +} diff --git a/crates/openfang-memory/src/mongo/indexes.rs b/crates/openfang-memory/src/mongo/indexes.rs new file mode 100644 index 000000000..0ff5851ad --- /dev/null +++ b/crates/openfang-memory/src/mongo/indexes.rs @@ -0,0 +1,154 @@ +//! MongoDB collection index setup. + +use mongodb::Database; +use mongodb::IndexModel; +use mongodb::options::IndexOptions; +use bson::doc; +use openfang_types::error::{OpenFangError, OpenFangResult}; + +/// Ensure all collections exist and indexes are created. +pub async fn ensure_indexes(db: &Database) -> OpenFangResult<()> { + // agents + db.collection::("agents") + .create_index( + IndexModel::builder() + .keys(doc! { "name": 1 }) + .options(IndexOptions::builder().unique(true).build()) + .build(), + ) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (agents): {e}")))?; + + // kv_store + db.collection::("kv_store") + .create_index( + IndexModel::builder() + .keys(doc! { "agent_id": 1, "key": 1 }) + .options(IndexOptions::builder().unique(true).build()) + .build(), + ) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (kv_store): {e}")))?; + + // sessions + let sessions = db.collection::("sessions"); + sessions + .create_index(IndexModel::builder().keys(doc! { "agent_id": 1 }).build()) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (sessions): {e}")))?; + sessions + .create_index( + IndexModel::builder() + .keys(doc! { "agent_id": 1, "label": 1 }) + .build(), + ) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (sessions label): {e}")))?; + + // memories + let memories = db.collection::("memories"); + memories + .create_index(IndexModel::builder().keys(doc! { "agent_id": 1 }).build()) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (memories agent): {e}")))?; + memories + .create_index( + IndexModel::builder() + .keys(doc! { "deleted": 1, "scope": 1 }) + .build(), + ) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (memories deleted): {e}")))?; + memories + .create_index( + IndexModel::builder() + .keys(doc! { "accessed_at": -1 }) + .build(), + ) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (memories accessed): {e}")))?; + + // entities + db.collection::("entities") + .create_index(IndexModel::builder().keys(doc! { "name": 1 }).build()) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (entities): {e}")))?; + + // relations + let relations = db.collection::("relations"); + relations + .create_index( + IndexModel::builder() + .keys(doc! { "source_entity": 1 }) + .build(), + ) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (relations src): {e}")))?; + relations + .create_index( + IndexModel::builder() + .keys(doc! { "target_entity": 1 }) + .build(), + ) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (relations tgt): {e}")))?; + relations + .create_index( + IndexModel::builder() + .keys(doc! { "relation_type": 1 }) + .build(), + ) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (relations type): {e}")))?; + + // usage_events + let usage = db.collection::("usage_events"); + usage + .create_index( + IndexModel::builder() + .keys(doc! { "agent_id": 1, "timestamp": -1 }) + .build(), + ) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (usage agent): {e}")))?; + usage + .create_index( + IndexModel::builder() + .keys(doc! { "timestamp": -1 }) + .build(), + ) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (usage ts): {e}")))?; + + // task_queue + db.collection::("task_queue") + .create_index( + IndexModel::builder() + .keys(doc! { "status": 1, "priority": -1, "created_at": 1 }) + .build(), + ) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (task_queue): {e}")))?; + + // audit_entries + let audit = db.collection::("audit_entries"); + audit + .create_index( + IndexModel::builder() + .keys(doc! { "seq": 1 }) + .options(IndexOptions::builder().unique(true).build()) + .build(), + ) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (audit seq): {e}")))?; + audit + .create_index( + IndexModel::builder() + .keys(doc! { "agent_id": 1 }) + .build(), + ) + .await + .map_err(|e| OpenFangError::Memory(format!("Index creation failed (audit agent): {e}")))?; + + Ok(()) +} diff --git a/crates/openfang-memory/src/mongo/knowledge.rs b/crates/openfang-memory/src/mongo/knowledge.rs new file mode 100644 index 000000000..7db0faf65 --- /dev/null +++ b/crates/openfang-memory/src/mongo/knowledge.rs @@ -0,0 +1,242 @@ +//! MongoDB knowledge graph store for entities and relations. + +use bson::doc; +use chrono::Utc; +use futures::TryStreamExt; +use mongodb::Collection; +use openfang_types::error::{OpenFangError, OpenFangResult}; +use openfang_types::memory::{ + Entity, EntityType, GraphMatch, GraphPattern, Relation, RelationType, +}; +use std::collections::HashMap; +use uuid::Uuid; + +/// Knowledge graph store backed by MongoDB. +#[derive(Clone)] +pub struct MongoKnowledgeStore { + entities: Collection, + relations: Collection, +} + +impl MongoKnowledgeStore { + pub fn new(db: mongodb::Database) -> Self { + Self { + entities: db.collection("entities"), + relations: db.collection("relations"), + } + } + + /// Add an entity to the knowledge graph. + pub async fn add_entity(&self, entity: Entity) -> OpenFangResult { + let id = if entity.id.is_empty() { + Uuid::new_v4().to_string() + } else { + entity.id.clone() + }; + let entity_type_str = serde_json::to_string(&entity.entity_type) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let props_str = serde_json::to_string(&entity.properties) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let now = bson::DateTime::from_chrono(Utc::now()); + + let filter = doc! { "_id": &id }; + let update = doc! { + "$set": { + "entity_type": &entity_type_str, + "name": &entity.name, + "properties": &props_str, + "updated_at": now, + }, + "$setOnInsert": { + "created_at": now, + }, + }; + self.entities + .update_one(filter, update) + .upsert(true) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(id) + } + + /// Add a relation between two entities. + pub async fn add_relation(&self, relation: Relation) -> OpenFangResult { + let id = Uuid::new_v4().to_string(); + let rel_type_str = serde_json::to_string(&relation.relation) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let props_str = serde_json::to_string(&relation.properties) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let now = bson::DateTime::from_chrono(Utc::now()); + + let doc = doc! { + "_id": &id, + "source_entity": &relation.source, + "relation_type": &rel_type_str, + "target_entity": &relation.target, + "properties": &props_str, + "confidence": relation.confidence as f64, + "created_at": now, + }; + self.relations + .insert_one(doc) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(id) + } + + /// Query the knowledge graph with a pattern. + pub async fn query_graph(&self, pattern: GraphPattern) -> OpenFangResult> { + // Build filter on relations + let mut filter = doc! {}; + if let Some(ref source) = pattern.source { + // Need to find entities matching by id or name first + let source_ids = self.resolve_entity_ids(source).await?; + if source_ids.is_empty() { + return Ok(Vec::new()); + } + filter.insert("source_entity", doc! { "$in": &source_ids }); + } + if let Some(ref relation) = pattern.relation { + let rel_str = serde_json::to_string(relation) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + filter.insert("relation_type", rel_str); + } + if let Some(ref target) = pattern.target { + let target_ids = self.resolve_entity_ids(target).await?; + if target_ids.is_empty() { + return Ok(Vec::new()); + } + filter.insert("target_entity", doc! { "$in": &target_ids }); + } + + let opts = mongodb::options::FindOptions::builder() + .limit(100) + .build(); + let mut cursor = self + .relations + .find(filter) + .with_options(opts) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut matches = Vec::new(); + while let Some(rel_doc) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + let source_id = rel_doc.get_str("source_entity").unwrap_or_default(); + let target_id = rel_doc.get_str("target_entity").unwrap_or_default(); + + let source_entity = self.load_entity(source_id).await?; + let target_entity = self.load_entity(target_id).await?; + + if let (Some(src), Some(tgt)) = (source_entity, target_entity) { + let relation = parse_relation_doc(&rel_doc); + matches.push(GraphMatch { + source: src, + relation, + target: tgt, + }); + } + } + + Ok(matches) + } + + /// Resolve an entity identifier (could be id or name) to matching entity IDs. + async fn resolve_entity_ids(&self, id_or_name: &str) -> OpenFangResult> { + let filter = doc! { + "$or": [ + { "_id": id_or_name }, + { "name": id_or_name }, + ] + }; + let opts = mongodb::options::FindOptions::builder() + .projection(doc! { "_id": 1 }) + .build(); + let mut cursor = self + .entities + .find(filter) + .with_options(opts) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut ids = Vec::new(); + while let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + if let Ok(id) = d.get_str("_id") { + ids.push(id.to_string()); + } + } + Ok(ids) + } + + /// Load a single entity by ID. + async fn load_entity(&self, id: &str) -> OpenFangResult> { + let doc = self + .entities + .find_one(doc! { "_id": id }) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(doc.as_ref().map(parse_entity_doc)) + } +} + +fn parse_entity_doc(d: &bson::Document) -> Entity { + let id = d.get_str("_id").unwrap_or_default().to_string(); + let etype_str = d.get_str("entity_type").unwrap_or("\"unknown\""); + let entity_type: EntityType = + serde_json::from_str(etype_str).unwrap_or(EntityType::Custom("unknown".to_string())); + let name = d.get_str("name").unwrap_or_default().to_string(); + let props_str = d.get_str("properties").unwrap_or("{}"); + let properties: HashMap = + serde_json::from_str(props_str).unwrap_or_default(); + let created_at = d + .get_datetime("created_at") + .ok() + .map(|dt| dt.to_chrono()) + .unwrap_or_else(Utc::now); + let updated_at = d + .get_datetime("updated_at") + .ok() + .map(|dt| dt.to_chrono()) + .unwrap_or_else(Utc::now); + + Entity { + id, + entity_type, + name, + properties, + created_at, + updated_at, + } +} + +fn parse_relation_doc(d: &bson::Document) -> Relation { + let source = d.get_str("source_entity").unwrap_or_default().to_string(); + let rtype_str = d.get_str("relation_type").unwrap_or("\"RelatedTo\""); + let relation: RelationType = serde_json::from_str(rtype_str).unwrap_or(RelationType::RelatedTo); + let target = d.get_str("target_entity").unwrap_or_default().to_string(); + let props_str = d.get_str("properties").unwrap_or("{}"); + let properties: HashMap = + serde_json::from_str(props_str).unwrap_or_default(); + let confidence = d.get_f64("confidence").unwrap_or(1.0) as f32; + let created_at = d + .get_datetime("created_at") + .ok() + .map(|dt| dt.to_chrono()) + .unwrap_or_else(Utc::now); + + Relation { + source, + relation, + target, + properties, + confidence, + created_at, + } +} diff --git a/crates/openfang-memory/src/mongo/mod.rs b/crates/openfang-memory/src/mongo/mod.rs new file mode 100644 index 000000000..812f59885 --- /dev/null +++ b/crates/openfang-memory/src/mongo/mod.rs @@ -0,0 +1,280 @@ +//! MongoDB backend for the memory substrate. +//! +//! Provides the same functionality as the SQLite backend but backed by MongoDB. +//! Each store maps to one or more MongoDB collections. + +pub mod consolidation; +pub mod indexes; +pub mod knowledge; +pub mod semantic; +pub mod session; +pub mod structured; +pub mod usage; + +use bson::doc; +use chrono::Utc; +use futures::TryStreamExt; +use mongodb::Collection; +use openfang_types::error::{OpenFangError, OpenFangResult}; + +use self::consolidation::MongoConsolidationEngine; +use self::knowledge::MongoKnowledgeStore; +use self::semantic::MongoSemanticStore; +use self::session::MongoSessionStore; +use self::structured::MongoStructuredStore; +use self::usage::MongoUsageStore; + +/// Composed MongoDB backend holding all store handles. +#[derive(Clone)] +pub struct MongoBackend { + /// The underlying MongoDB database handle. + pub db: mongodb::Database, + /// KV + agent persistence. + pub structured: MongoStructuredStore, + /// Semantic memory with embeddings. + pub semantic: MongoSemanticStore, + /// Knowledge graph (entities + relations). + pub knowledge: MongoKnowledgeStore, + /// Session management. + pub sessions: MongoSessionStore, + /// Usage / cost tracking. + pub usage: MongoUsageStore, + /// Memory decay engine. + pub consolidation: MongoConsolidationEngine, + /// Paired devices collection. + paired_devices: Collection, + /// Task queue collection. + task_queue: Collection, +} + +impl MongoBackend { + /// Create a new MongoDB backend from a database handle. + pub fn new(db: mongodb::Database, decay_rate: f32) -> Self { + Self { + structured: MongoStructuredStore::new(db.clone()), + semantic: MongoSemanticStore::new(db.clone()), + knowledge: MongoKnowledgeStore::new(db.clone()), + sessions: MongoSessionStore::new(db.clone()), + usage: MongoUsageStore::new(db.clone()), + consolidation: MongoConsolidationEngine::new(db.clone(), decay_rate), + paired_devices: db.collection("paired_devices"), + task_queue: db.collection("task_queue"), + db, + } + } + + // ----------------------------------------------------------------- + // Paired devices + // ----------------------------------------------------------------- + + pub async fn load_paired_devices(&self) -> OpenFangResult> { + let mut cursor = self + .paired_devices + .find(doc! {}) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut devices = Vec::new(); + while let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + devices.push(serde_json::json!({ + "device_id": d.get_str("_id").unwrap_or_default(), + "display_name": d.get_str("display_name").unwrap_or_default(), + "platform": d.get_str("platform").unwrap_or_default(), + "paired_at": d.get_str("paired_at").unwrap_or_default(), + "last_seen": d.get_str("last_seen").unwrap_or_default(), + "push_token": d.get_str("push_token").ok(), + })); + } + Ok(devices) + } + + pub async fn save_paired_device( + &self, + device_id: &str, + display_name: &str, + platform: &str, + paired_at: &str, + last_seen: &str, + push_token: Option<&str>, + ) -> OpenFangResult<()> { + let filter = doc! { "_id": device_id }; + let update = doc! { + "$set": { + "display_name": display_name, + "platform": platform, + "paired_at": paired_at, + "last_seen": last_seen, + "push_token": push_token, + } + }; + self.paired_devices + .update_one(filter, update) + .upsert(true) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + pub async fn remove_paired_device(&self, device_id: &str) -> OpenFangResult<()> { + self.paired_devices + .delete_one(doc! { "_id": device_id }) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + // ----------------------------------------------------------------- + // Task queue + // ----------------------------------------------------------------- + + pub async fn task_post( + &self, + title: &str, + description: &str, + assigned_to: Option<&str>, + created_by: Option<&str>, + ) -> OpenFangResult { + let id = uuid::Uuid::new_v4().to_string(); + let now = bson::DateTime::from_chrono(Utc::now()); + let doc = doc! { + "_id": &id, + "agent_id": created_by.unwrap_or(""), + "task_type": title, + "payload": bson::Binary { subtype: bson::spec::BinarySubtype::Generic, bytes: vec![] }, + "status": "pending", + "priority": 0_i32, + "created_at": now, + "title": title, + "description": description, + "assigned_to": assigned_to.unwrap_or(""), + "created_by": created_by.unwrap_or(""), + }; + self.task_queue + .insert_one(doc) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(id) + } + + pub async fn task_claim(&self, agent_id: &str) -> OpenFangResult> { + let filter = doc! { + "status": "pending", + "$or": [ + { "assigned_to": agent_id }, + { "assigned_to": "" }, + ] + }; + let opts = mongodb::options::FindOneOptions::builder() + .sort(doc! { "priority": -1, "created_at": 1 }) + .build(); + let doc = self + .task_queue + .find_one(filter) + .with_options(opts) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + match doc { + Some(d) => { + let id = d.get_str("_id").unwrap_or_default().to_string(); + let title = d.get_str("title").unwrap_or_default().to_string(); + let description = d.get_str("description").unwrap_or_default().to_string(); + let assigned = d.get_str("assigned_to").unwrap_or_default().to_string(); + let created_by = d.get_str("created_by").unwrap_or_default().to_string(); + let created_at = d + .get_datetime("created_at") + .ok() + .map(|dt| dt.to_chrono().to_rfc3339()) + .unwrap_or_default(); + + self.task_queue + .update_one( + doc! { "_id": &id }, + doc! { "$set": { "status": "in_progress", "assigned_to": agent_id } }, + ) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + Ok(Some(serde_json::json!({ + "id": id, + "title": title, + "description": description, + "status": "in_progress", + "assigned_to": if assigned.is_empty() { agent_id } else { &assigned }, + "created_by": created_by, + "created_at": created_at, + }))) + } + None => Ok(None), + } + } + + pub async fn task_complete(&self, task_id: &str, result: &str) -> OpenFangResult<()> { + let now = bson::DateTime::from_chrono(Utc::now()); + let res = self + .task_queue + .update_one( + doc! { "_id": task_id }, + doc! { "$set": { "status": "completed", "result": result, "completed_at": now } }, + ) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + if res.matched_count == 0 { + return Err(OpenFangError::Internal(format!("Task not found: {task_id}"))); + } + Ok(()) + } + + pub async fn task_list( + &self, + status: Option<&str>, + ) -> OpenFangResult> { + let filter = match status { + Some(s) => doc! { "status": s }, + None => doc! {}, + }; + let opts = mongodb::options::FindOptions::builder() + .sort(doc! { "created_at": -1 }) + .build(); + let mut cursor = self + .task_queue + .find(filter) + .with_options(opts) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut tasks = Vec::new(); + while let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + let created_at = d + .get_datetime("created_at") + .ok() + .map(|dt| dt.to_chrono().to_rfc3339()) + .unwrap_or_default(); + let completed_at = d + .get_datetime("completed_at") + .ok() + .map(|dt| dt.to_chrono().to_rfc3339()); + + tasks.push(serde_json::json!({ + "id": d.get_str("_id").unwrap_or_default(), + "title": d.get_str("title").unwrap_or_default(), + "description": d.get_str("description").unwrap_or_default(), + "status": d.get_str("status").unwrap_or_default(), + "assigned_to": d.get_str("assigned_to").unwrap_or_default(), + "created_by": d.get_str("created_by").unwrap_or_default(), + "created_at": created_at, + "completed_at": completed_at, + "result": d.get_str("result").ok(), + })); + } + Ok(tasks) + } +} diff --git a/crates/openfang-memory/src/mongo/semantic.rs b/crates/openfang-memory/src/mongo/semantic.rs new file mode 100644 index 000000000..dd8fb08aa --- /dev/null +++ b/crates/openfang-memory/src/mongo/semantic.rs @@ -0,0 +1,312 @@ +//! MongoDB semantic memory store with vector embedding support. + +use bson::doc; +use chrono::Utc; +use futures::TryStreamExt; +use mongodb::Collection; +use openfang_types::agent::AgentId; +use openfang_types::error::{OpenFangError, OpenFangResult}; +use openfang_types::memory::{MemoryFilter, MemoryFragment, MemoryId, MemorySource}; +use std::collections::HashMap; +use tracing::debug; + +/// Semantic store backed by MongoDB with optional vector search. +#[derive(Clone)] +pub struct MongoSemanticStore { + memories: Collection, +} + +impl MongoSemanticStore { + pub fn new(db: mongodb::Database) -> Self { + Self { + memories: db.collection("memories"), + } + } + + /// Store a new memory fragment (without embedding). + pub async fn remember( + &self, + agent_id: AgentId, + content: &str, + source: MemorySource, + scope: &str, + metadata: HashMap, + ) -> OpenFangResult { + self.remember_with_embedding(agent_id, content, source, scope, metadata, None) + .await + } + + /// Store a new memory fragment with an optional embedding vector. + pub async fn remember_with_embedding( + &self, + agent_id: AgentId, + content: &str, + source: MemorySource, + scope: &str, + metadata: HashMap, + embedding: Option<&[f32]>, + ) -> OpenFangResult { + let id = MemoryId::new(); + let now = bson::DateTime::from_chrono(Utc::now()); + let source_str = serde_json::to_string(&source) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let meta_str = serde_json::to_string(&metadata) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + + let mut doc = doc! { + "_id": id.0.to_string(), + "agent_id": agent_id.0.to_string(), + "content": content, + "source": &source_str, + "scope": scope, + "confidence": 1.0_f64, + "metadata": &meta_str, + "created_at": now, + "accessed_at": now, + "access_count": 0_i64, + "deleted": false, + }; + + if let Some(emb) = embedding { + let bson_emb: Vec = emb.iter().map(|&v| bson::Bson::Double(v as f64)).collect(); + doc.insert("embedding", bson_emb); + } + + self.memories + .insert_one(doc) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(id) + } + + /// Search for memories using text matching (fallback, no embeddings). + pub async fn recall( + &self, + query: &str, + limit: usize, + filter: Option, + ) -> OpenFangResult> { + self.recall_with_embedding(query, limit, filter, None).await + } + + /// Search for memories using vector similarity when a query embedding is provided, + /// falling back to regex matching otherwise. + pub async fn recall_with_embedding( + &self, + query: &str, + limit: usize, + filter: Option, + query_embedding: Option<&[f32]>, + ) -> OpenFangResult> { + let fetch_limit = if query_embedding.is_some() { + (limit * 10).max(100) + } else { + limit + }; + + let mut filter_doc = doc! { "deleted": false }; + + // Text search filter (only when no embeddings) + if query_embedding.is_none() && !query.is_empty() { + // Escape regex special characters for safe literal matching + let escaped = regex_escape(query); + filter_doc.insert("content", doc! { "$regex": &escaped, "$options": "i" }); + } + + // Apply filters + if let Some(ref f) = filter { + if let Some(agent_id) = f.agent_id { + filter_doc.insert("agent_id", agent_id.0.to_string()); + } + if let Some(ref scope) = f.scope { + filter_doc.insert("scope", scope.as_str()); + } + if let Some(min_conf) = f.min_confidence { + filter_doc.insert("confidence", doc! { "$gte": min_conf as f64 }); + } + if let Some(ref source) = f.source { + let source_str = serde_json::to_string(source) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + filter_doc.insert("source", source_str); + } + } + + let opts = mongodb::options::FindOptions::builder() + .sort(doc! { "accessed_at": -1, "access_count": -1 }) + .limit(fetch_limit as i64) + .build(); + + let mut cursor = self + .memories + .find(filter_doc) + .with_options(opts) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut fragments = Vec::new(); + while let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + if let Some(frag) = parse_memory_doc(&d) { + fragments.push(frag); + } + } + + // If we have a query embedding, re-rank by cosine similarity + if let Some(qe) = query_embedding { + fragments.sort_by(|a, b| { + let sim_a = a + .embedding + .as_deref() + .map(|e| cosine_similarity(qe, e)) + .unwrap_or(-1.0); + let sim_b = b + .embedding + .as_deref() + .map(|e| cosine_similarity(qe, e)) + .unwrap_or(-1.0); + sim_b + .partial_cmp(&sim_a) + .unwrap_or(std::cmp::Ordering::Equal) + }); + fragments.truncate(limit); + debug!( + "Vector recall: {} results from {} candidates", + fragments.len(), + fetch_limit + ); + } + + // Update access counts for returned memories + let now = bson::DateTime::from_chrono(Utc::now()); + for frag in &fragments { + let _ = self + .memories + .update_one( + doc! { "_id": frag.id.0.to_string() }, + doc! { "$inc": { "access_count": 1_i64 }, "$set": { "accessed_at": now } }, + ) + .await; + } + + Ok(fragments) + } + + /// Soft-delete a memory fragment. + pub async fn forget(&self, id: MemoryId) -> OpenFangResult<()> { + self.memories + .update_one( + doc! { "_id": id.0.to_string() }, + doc! { "$set": { "deleted": true } }, + ) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + /// Update the embedding for an existing memory. + pub async fn update_embedding(&self, id: MemoryId, embedding: &[f32]) -> OpenFangResult<()> { + let bson_emb: Vec = embedding.iter().map(|&v| bson::Bson::Double(v as f64)).collect(); + self.memories + .update_one( + doc! { "_id": id.0.to_string() }, + doc! { "$set": { "embedding": bson_emb } }, + ) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } +} + +fn parse_memory_doc(d: &bson::Document) -> Option { + let id_str = d.get_str("_id").ok()?; + let id = uuid::Uuid::parse_str(id_str).ok().map(MemoryId)?; + + let agent_str = d.get_str("agent_id").unwrap_or_default(); + let agent_id = uuid::Uuid::parse_str(agent_str) + .map(openfang_types::agent::AgentId) + .ok()?; + + let content = d.get_str("content").unwrap_or_default().to_string(); + + let source_str = d.get_str("source").unwrap_or("\"system\""); + let source: MemorySource = serde_json::from_str(source_str).unwrap_or(MemorySource::System); + + let scope = d.get_str("scope").unwrap_or("episodic").to_string(); + + let confidence = d.get_f64("confidence").unwrap_or(1.0) as f32; + + let meta_str = d.get_str("metadata").unwrap_or("{}"); + let metadata: HashMap = + serde_json::from_str(meta_str).unwrap_or_default(); + + let created_at = d + .get_datetime("created_at") + .ok() + .map(|dt| dt.to_chrono()) + .unwrap_or_else(Utc::now); + + let accessed_at = d + .get_datetime("accessed_at") + .ok() + .map(|dt| dt.to_chrono()) + .unwrap_or_else(Utc::now); + + let access_count = d.get_i64("access_count").unwrap_or(0) as u64; + + let embedding = d.get_array("embedding").ok().map(|arr| { + arr.iter() + .filter_map(|v| v.as_f64().map(|f| f as f32)) + .collect::>() + }); + + Some(MemoryFragment { + id, + agent_id, + content, + embedding, + metadata, + source, + confidence, + created_at, + accessed_at, + access_count, + scope, + }) +} + +/// Compute cosine similarity between two vectors. +fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 { + if a.len() != b.len() || a.is_empty() { + return 0.0; + } + let mut dot = 0.0f32; + let mut norm_a = 0.0f32; + let mut norm_b = 0.0f32; + for i in 0..a.len() { + dot += a[i] * b[i]; + norm_a += a[i] * a[i]; + norm_b += b[i] * b[i]; + } + let denom = norm_a.sqrt() * norm_b.sqrt(); + if denom < f32::EPSILON { + 0.0 + } else { + dot / denom + } +} + +/// Escape regex special characters for safe use in MongoDB $regex. +fn regex_escape(s: &str) -> String { + let special = ['.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\', '^', '$', '|']; + let mut escaped = String::with_capacity(s.len() * 2); + for c in s.chars() { + if special.contains(&c) { + escaped.push('\\'); + } + escaped.push(c); + } + escaped +} diff --git a/crates/openfang-memory/src/mongo/session.rs b/crates/openfang-memory/src/mongo/session.rs new file mode 100644 index 000000000..6076ebe53 --- /dev/null +++ b/crates/openfang-memory/src/mongo/session.rs @@ -0,0 +1,461 @@ +//! MongoDB session management — load/save conversation history. + +use bson::doc; +use chrono::Utc; +use futures::TryStreamExt; +use mongodb::Collection; +use openfang_types::agent::{AgentId, SessionId}; +use openfang_types::error::{OpenFangError, OpenFangResult}; +use openfang_types::message::Message; +use std::path::Path; + +use crate::session::{CanonicalSession, Session}; + +/// Default number of recent messages to include from canonical session. +const DEFAULT_CANONICAL_WINDOW: usize = 50; + +/// Default compaction threshold: when message count exceeds this, compact older messages. +const DEFAULT_COMPACTION_THRESHOLD: usize = 100; + +/// Session store backed by MongoDB. +#[derive(Clone)] +pub struct MongoSessionStore { + sessions: Collection, + canonical: Collection, +} + +impl MongoSessionStore { + pub fn new(db: mongodb::Database) -> Self { + Self { + sessions: db.collection("sessions"), + canonical: db.collection("canonical_sessions"), + } + } + + /// Load a session from the database. + pub async fn get_session(&self, session_id: SessionId) -> OpenFangResult> { + let doc = self + .sessions + .find_one(doc! { "_id": session_id.0.to_string() }) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + match doc { + Some(d) => { + let agent_str = d.get_str("agent_id").unwrap_or_default(); + let agent_id = uuid::Uuid::parse_str(agent_str) + .map(AgentId) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + let messages_bytes = d + .get_binary_generic("messages") + .map_err(|_| OpenFangError::Memory("Missing messages field".into()))?; + let messages: Vec = rmp_serde::from_slice(messages_bytes) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let tokens = d.get_i64("context_window_tokens").unwrap_or(0) as u64; + let label = d.get_str("label").ok().map(|s| s.to_string()); + + Ok(Some(Session { + id: session_id, + agent_id, + messages, + context_window_tokens: tokens, + label, + })) + } + None => Ok(None), + } + } + + /// Save a session to the database. + pub async fn save_session(&self, session: &Session) -> OpenFangResult<()> { + let messages_blob = rmp_serde::to_vec_named(&session.messages) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let now = bson::DateTime::from_chrono(Utc::now()); + + let filter = doc! { "_id": session.id.0.to_string() }; + let update = doc! { + "$set": { + "agent_id": session.agent_id.0.to_string(), + "messages": bson::Binary { subtype: bson::spec::BinarySubtype::Generic, bytes: messages_blob }, + "context_window_tokens": session.context_window_tokens as i64, + "label": session.label.as_deref(), + "updated_at": now, + }, + "$setOnInsert": { + "created_at": now, + }, + }; + self.sessions + .update_one(filter, update) + .upsert(true) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + /// Delete a session from the database. + pub async fn delete_session(&self, session_id: SessionId) -> OpenFangResult<()> { + self.sessions + .delete_one(doc! { "_id": session_id.0.to_string() }) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + /// Delete all sessions belonging to an agent. + pub async fn delete_agent_sessions(&self, agent_id: AgentId) -> OpenFangResult<()> { + self.sessions + .delete_many(doc! { "agent_id": agent_id.0.to_string() }) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + /// Delete the canonical (cross-channel) session for an agent. + pub async fn delete_canonical_session(&self, agent_id: AgentId) -> OpenFangResult<()> { + self.canonical + .delete_one(doc! { "_id": agent_id.0.to_string() }) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + /// List all sessions with metadata. + pub async fn list_sessions(&self) -> OpenFangResult> { + let opts = mongodb::options::FindOptions::builder() + .sort(doc! { "created_at": -1 }) + .build(); + let mut cursor = self + .sessions + .find(doc! {}) + .with_options(opts) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut sessions = Vec::new(); + while let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + let session_id = d.get_str("_id").unwrap_or_default().to_string(); + let agent_id = d.get_str("agent_id").unwrap_or_default().to_string(); + let label = d.get_str("label").ok().map(|s| s.to_string()); + let created_at = d + .get_datetime("created_at") + .ok() + .map(|dt| dt.to_chrono().to_rfc3339()) + .unwrap_or_default(); + let msg_count = d + .get_binary_generic("messages") + .ok() + .and_then(|b| rmp_serde::from_slice::>(b).ok()) + .map(|m| m.len()) + .unwrap_or(0); + + sessions.push(serde_json::json!({ + "session_id": session_id, + "agent_id": agent_id, + "message_count": msg_count, + "created_at": created_at, + "label": label, + })); + } + Ok(sessions) + } + + /// Create a new empty session for an agent. + pub async fn create_session(&self, agent_id: AgentId) -> OpenFangResult { + let session = Session { + id: SessionId::new(), + agent_id, + messages: Vec::new(), + context_window_tokens: 0, + label: None, + }; + self.save_session(&session).await?; + Ok(session) + } + + /// Set the label on an existing session. + pub async fn set_session_label( + &self, + session_id: SessionId, + label: Option<&str>, + ) -> OpenFangResult<()> { + let now = bson::DateTime::from_chrono(Utc::now()); + self.sessions + .update_one( + doc! { "_id": session_id.0.to_string() }, + doc! { "$set": { "label": label, "updated_at": now } }, + ) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + /// Find a session by label for a given agent. + pub async fn find_session_by_label( + &self, + agent_id: AgentId, + label: &str, + ) -> OpenFangResult> { + let doc = self + .sessions + .find_one(doc! { "agent_id": agent_id.0.to_string(), "label": label }) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + match doc { + Some(d) => { + let id_str = d.get_str("_id").unwrap_or_default(); + let session_id = uuid::Uuid::parse_str(id_str) + .map(SessionId) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + let messages_bytes = d + .get_binary_generic("messages") + .map_err(|_| OpenFangError::Memory("Missing messages field".into()))?; + let messages: Vec = rmp_serde::from_slice(messages_bytes) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let tokens = d.get_i64("context_window_tokens").unwrap_or(0) as u64; + let lbl = d.get_str("label").ok().map(|s| s.to_string()); + + Ok(Some(Session { + id: session_id, + agent_id, + messages, + context_window_tokens: tokens, + label: lbl, + })) + } + None => Ok(None), + } + } + + /// List all sessions for a specific agent. + pub async fn list_agent_sessions( + &self, + agent_id: AgentId, + ) -> OpenFangResult> { + let opts = mongodb::options::FindOptions::builder() + .sort(doc! { "created_at": -1 }) + .build(); + let mut cursor = self + .sessions + .find(doc! { "agent_id": agent_id.0.to_string() }) + .with_options(opts) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut sessions = Vec::new(); + while let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + let session_id = d.get_str("_id").unwrap_or_default().to_string(); + let label = d.get_str("label").ok().map(|s| s.to_string()); + let created_at = d + .get_datetime("created_at") + .ok() + .map(|dt| dt.to_chrono().to_rfc3339()) + .unwrap_or_default(); + let msg_count = d + .get_binary_generic("messages") + .ok() + .and_then(|b| rmp_serde::from_slice::>(b).ok()) + .map(|m| m.len()) + .unwrap_or(0); + + sessions.push(serde_json::json!({ + "session_id": session_id, + "message_count": msg_count, + "created_at": created_at, + "label": label, + })); + } + Ok(sessions) + } + + /// Create a new session with an optional label. + pub async fn create_session_with_label( + &self, + agent_id: AgentId, + label: Option<&str>, + ) -> OpenFangResult { + let session = Session { + id: SessionId::new(), + agent_id, + messages: Vec::new(), + context_window_tokens: 0, + label: label.map(|s| s.to_string()), + }; + self.save_session(&session).await?; + Ok(session) + } + + /// Store an LLM-generated summary, replacing older messages with the summary + /// and keeping only the specified recent messages. + pub async fn store_llm_summary( + &self, + agent_id: AgentId, + summary: &str, + kept_messages: Vec, + ) -> OpenFangResult<()> { + let mut canonical = self.load_canonical(agent_id).await?; + canonical.compacted_summary = Some(summary.to_string()); + canonical.messages = kept_messages; + canonical.compaction_cursor = 0; + canonical.updated_at = Utc::now().to_rfc3339(); + self.save_canonical(&canonical).await + } + + /// Load the canonical session for an agent, creating one if it doesn't exist. + pub async fn load_canonical(&self, agent_id: AgentId) -> OpenFangResult { + let doc = self + .canonical + .find_one(doc! { "_id": agent_id.0.to_string() }) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + match doc { + Some(d) => { + let messages_bytes = d + .get_binary_generic("messages") + .map_err(|_| OpenFangError::Memory("Missing messages field".into()))?; + let messages: Vec = rmp_serde::from_slice(messages_bytes) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let cursor = d.get_i64("compaction_cursor").unwrap_or(0) as usize; + let summary = d.get_str("compacted_summary").ok().map(|s| s.to_string()); + let updated_at = d + .get_datetime("updated_at") + .ok() + .map(|dt| dt.to_chrono().to_rfc3339()) + .unwrap_or_else(|| Utc::now().to_rfc3339()); + + Ok(CanonicalSession { + agent_id, + messages, + compaction_cursor: cursor, + compacted_summary: summary, + updated_at, + }) + } + None => { + let now = Utc::now().to_rfc3339(); + Ok(CanonicalSession { + agent_id, + messages: Vec::new(), + compaction_cursor: 0, + compacted_summary: None, + updated_at: now, + }) + } + } + } + + /// Append new messages to the canonical session and compact if over threshold. + pub async fn append_canonical( + &self, + agent_id: AgentId, + new_messages: &[Message], + compaction_threshold: Option, + ) -> OpenFangResult { + let mut canonical = self.load_canonical(agent_id).await?; + canonical.messages.extend(new_messages.iter().cloned()); + + let threshold = compaction_threshold.unwrap_or(DEFAULT_COMPACTION_THRESHOLD); + + // Compact if over threshold + if canonical.messages.len() > threshold { + let keep_count = DEFAULT_CANONICAL_WINDOW; + let to_compact = canonical.messages.len().saturating_sub(keep_count); + if to_compact > canonical.compaction_cursor { + let compacting = &canonical.messages[canonical.compaction_cursor..to_compact]; + let mut summary_parts: Vec = Vec::new(); + if let Some(ref existing) = canonical.compacted_summary { + summary_parts.push(existing.clone()); + } + for msg in compacting { + let role = match msg.role { + openfang_types::message::Role::User => "User", + openfang_types::message::Role::Assistant => "Assistant", + openfang_types::message::Role::System => "System", + }; + let text = msg.content.text_content(); + if !text.is_empty() { + let truncated = if text.len() > 200 { + format!("{}...", openfang_types::truncate_str(&text, 200)) + } else { + text + }; + summary_parts.push(format!("{role}: {truncated}")); + } + } + let mut full_summary = summary_parts.join("\n"); + if full_summary.len() > 4000 { + let start = full_summary.len() - 4000; + let safe_start = (start..full_summary.len()) + .find(|&i| full_summary.is_char_boundary(i)) + .unwrap_or(full_summary.len()); + full_summary = full_summary[safe_start..].to_string(); + } + canonical.compacted_summary = Some(full_summary); + canonical.compaction_cursor = to_compact; + canonical.messages = canonical.messages.split_off(to_compact); + canonical.compaction_cursor = 0; + } + } + + canonical.updated_at = Utc::now().to_rfc3339(); + self.save_canonical(&canonical).await?; + Ok(canonical) + } + + /// Get recent messages from canonical session for context injection. + pub async fn canonical_context( + &self, + agent_id: AgentId, + window_size: Option, + ) -> OpenFangResult<(Option, Vec)> { + let canonical = self.load_canonical(agent_id).await?; + let window = window_size.unwrap_or(DEFAULT_CANONICAL_WINDOW); + let start = canonical.messages.len().saturating_sub(window); + let recent = canonical.messages[start..].to_vec(); + Ok((canonical.compacted_summary.clone(), recent)) + } + + /// Persist a canonical session to MongoDB. + async fn save_canonical(&self, canonical: &CanonicalSession) -> OpenFangResult<()> { + let messages_blob = rmp_serde::to_vec(&canonical.messages) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let now = bson::DateTime::from_chrono(Utc::now()); + + let filter = doc! { "_id": canonical.agent_id.0.to_string() }; + let update = doc! { + "$set": { + "messages": bson::Binary { subtype: bson::spec::BinarySubtype::Generic, bytes: messages_blob }, + "compaction_cursor": canonical.compaction_cursor as i64, + "compacted_summary": &canonical.compacted_summary, + "updated_at": now, + }, + }; + self.canonical + .update_one(filter, update) + .upsert(true) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + /// Write a human-readable JSONL mirror of a session to disk. + /// (Same as SQLite version — file I/O only, no DB involved.) + pub fn write_jsonl_mirror( + &self, + session: &Session, + sessions_dir: &Path, + ) -> Result<(), std::io::Error> { + // Delegate to the shared implementation + crate::session::write_jsonl_mirror_impl(session, sessions_dir) + } +} diff --git a/crates/openfang-memory/src/mongo/structured.rs b/crates/openfang-memory/src/mongo/structured.rs new file mode 100644 index 000000000..af55b75c0 --- /dev/null +++ b/crates/openfang-memory/src/mongo/structured.rs @@ -0,0 +1,298 @@ +//! MongoDB structured store for key-value pairs and agent persistence. + +use bson::doc; +use chrono::Utc; +use futures::TryStreamExt; +use mongodb::Collection; +use openfang_types::agent::{AgentEntry, AgentId}; +use openfang_types::error::{OpenFangError, OpenFangResult}; + +/// Structured store backed by MongoDB for key-value operations and agent storage. +#[derive(Clone)] +pub struct MongoStructuredStore { + kv: Collection, + agents: Collection, +} + +impl MongoStructuredStore { + pub fn new(db: mongodb::Database) -> Self { + Self { + kv: db.collection("kv_store"), + agents: db.collection("agents"), + } + } + + pub async fn get( + &self, + agent_id: AgentId, + key: &str, + ) -> OpenFangResult> { + let filter = doc! { "agent_id": agent_id.0.to_string(), "key": key }; + let doc = self + .kv + .find_one(filter) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + match doc { + Some(d) => { + let bson_val = d + .get("value") + .ok_or_else(|| OpenFangError::Memory("Missing value field".into()))?; + let json_val: serde_json::Value = bson::from_bson(bson_val.clone()) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + Ok(Some(json_val)) + } + None => Ok(None), + } + } + + pub async fn set( + &self, + agent_id: AgentId, + key: &str, + value: serde_json::Value, + ) -> OpenFangResult<()> { + let bson_val = + bson::to_bson(&value).map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let now = bson::DateTime::from_chrono(Utc::now()); + let filter = doc! { "agent_id": agent_id.0.to_string(), "key": key }; + let update = doc! { + "$set": { + "agent_id": agent_id.0.to_string(), + "key": key, + "value": bson_val, + "updated_at": now, + }, + "$inc": { "version": 1_i32 }, + }; + self.kv + .update_one(filter, update) + .upsert(true) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + pub async fn delete(&self, agent_id: AgentId, key: &str) -> OpenFangResult<()> { + let filter = doc! { "agent_id": agent_id.0.to_string(), "key": key }; + self.kv + .delete_one(filter) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + pub async fn list_kv( + &self, + agent_id: AgentId, + ) -> OpenFangResult> { + let filter = doc! { "agent_id": agent_id.0.to_string() }; + let opts = mongodb::options::FindOptions::builder() + .sort(doc! { "key": 1 }) + .build(); + let mut cursor = self + .kv + .find(filter) + .with_options(opts) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut pairs = Vec::new(); + while let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + let key = d.get_str("key").unwrap_or_default().to_string(); + let value: serde_json::Value = d + .get("value") + .and_then(|v| bson::from_bson(v.clone()).ok()) + .unwrap_or(serde_json::Value::Null); + pairs.push((key, value)); + } + Ok(pairs) + } + + pub async fn save_agent(&self, entry: &AgentEntry) -> OpenFangResult<()> { + let manifest_blob = rmp_serde::to_vec_named(&entry.manifest) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let state_str = serde_json::to_string(&entry.state) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let identity_json = serde_json::to_string(&entry.identity) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + let now = bson::DateTime::from_chrono(Utc::now()); + + let filter = doc! { "_id": entry.id.0.to_string() }; + let update = doc! { + "$set": { + "name": &entry.name, + "manifest": bson::Binary { subtype: bson::spec::BinarySubtype::Generic, bytes: manifest_blob }, + "state": &state_str, + "updated_at": now, + "session_id": entry.session_id.0.to_string(), + "identity": &identity_json, + }, + "$setOnInsert": { + "created_at": bson::DateTime::from_chrono(entry.created_at), + }, + }; + self.agents + .update_one(filter, update) + .upsert(true) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + pub async fn load_agent(&self, agent_id: AgentId) -> OpenFangResult> { + let filter = doc! { "_id": agent_id.0.to_string() }; + let doc = self + .agents + .find_one(filter) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + match doc { + Some(d) => parse_agent_doc(agent_id, &d), + None => Ok(None), + } + } + + pub async fn remove_agent(&self, agent_id: AgentId) -> OpenFangResult<()> { + let filter = doc! { "_id": agent_id.0.to_string() }; + self.agents + .delete_one(filter) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + pub async fn load_all_agents(&self) -> OpenFangResult> { + let mut cursor = self + .agents + .find(doc! {}) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut agents = Vec::new(); + let mut seen_names = std::collections::HashSet::new(); + + while let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + let id_str = d.get_str("_id").unwrap_or_default(); + let name = d.get_str("name").unwrap_or_default(); + let name_lower = name.to_lowercase(); + if !seen_names.insert(name_lower) { + tracing::info!(agent = %name, id = %id_str, "Skipping duplicate agent name"); + continue; + } + + let agent_id = + match uuid::Uuid::parse_str(id_str).map(openfang_types::agent::AgentId) { + Ok(id) => id, + Err(e) => { + tracing::warn!(agent = %name, "Skipping agent with bad UUID '{id_str}': {e}"); + continue; + } + }; + + match parse_agent_doc(agent_id, &d) { + Ok(Some(entry)) => agents.push(entry), + Ok(None) => {} + Err(e) => { + tracing::warn!(agent = %name, id = %id_str, "Skipping agent: {e}"); + continue; + } + } + } + Ok(agents) + } + + pub async fn list_agents(&self) -> OpenFangResult> { + let opts = mongodb::options::FindOptions::builder() + .projection(doc! { "_id": 1, "name": 1, "state": 1 }) + .build(); + let mut cursor = self + .agents + .find(doc! {}) + .with_options(opts) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut agents = Vec::new(); + while let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + let id = d.get_str("_id").unwrap_or_default().to_string(); + let name = d.get_str("name").unwrap_or_default().to_string(); + let state = d.get_str("state").unwrap_or_default().to_string(); + agents.push((id, name, state)); + } + Ok(agents) + } +} + +fn parse_agent_doc( + agent_id: AgentId, + d: &bson::Document, +) -> OpenFangResult> { + let name = d.get_str("name").unwrap_or_default().to_string(); + + let manifest_binary = match d.get_binary_generic("manifest") { + Ok(bytes) => bytes.to_vec(), + Err(_) => { + tracing::warn!(agent = %name, "Skipping agent with missing manifest"); + return Ok(None); + } + }; + let manifest: openfang_types::agent::AgentManifest = match rmp_serde::from_slice(&manifest_binary) { + Ok(m) => m, + Err(e) => { + tracing::warn!( + agent = %name, + "Skipping agent with incompatible manifest: {e}" + ); + return Ok(None); + } + }; + + let state_str = d.get_str("state").unwrap_or("\"created\""); + let state: openfang_types::agent::AgentState = + serde_json::from_str(state_str).unwrap_or(openfang_types::agent::AgentState::Created); + + let created_at = d + .get_datetime("created_at") + .ok() + .map(|dt| dt.to_chrono()) + .unwrap_or_else(Utc::now); + + let session_id_str = d.get_str("session_id").unwrap_or(""); + let session_id = uuid::Uuid::parse_str(session_id_str) + .map(openfang_types::agent::SessionId) + .unwrap_or_else(|_| openfang_types::agent::SessionId::new()); + + let identity_str = d.get_str("identity").unwrap_or("{}"); + let identity = serde_json::from_str(identity_str).unwrap_or_default(); + + Ok(Some(AgentEntry { + id: agent_id, + name, + manifest, + state, + mode: Default::default(), + created_at, + last_active: Utc::now(), + parent: None, + children: vec![], + session_id, + tags: vec![], + identity, + onboarding_completed: false, + onboarding_completed_at: None, + })) +} diff --git a/crates/openfang-memory/src/mongo/usage.rs b/crates/openfang-memory/src/mongo/usage.rs new file mode 100644 index 000000000..61d8a062a --- /dev/null +++ b/crates/openfang-memory/src/mongo/usage.rs @@ -0,0 +1,294 @@ +//! MongoDB usage tracking store — records LLM usage events for cost monitoring. + +use bson::doc; +use chrono::{Datelike, Utc}; +use futures::TryStreamExt; +use mongodb::Collection; +use openfang_types::agent::AgentId; +use openfang_types::error::{OpenFangError, OpenFangResult}; + +use crate::usage::{DailyBreakdown, ModelUsage, UsageRecord, UsageSummary}; + +/// Usage store backed by MongoDB. +#[derive(Clone)] +pub struct MongoUsageStore { + events: Collection, +} + +impl MongoUsageStore { + pub fn new(db: mongodb::Database) -> Self { + Self { + events: db.collection("usage_events"), + } + } + + /// Record a usage event. + pub async fn record(&self, record: &UsageRecord) -> OpenFangResult<()> { + let id = uuid::Uuid::new_v4().to_string(); + let now = bson::DateTime::from_chrono(Utc::now()); + let doc = doc! { + "_id": &id, + "agent_id": record.agent_id.0.to_string(), + "timestamp": now, + "model": &record.model, + "input_tokens": record.input_tokens as i64, + "output_tokens": record.output_tokens as i64, + "cost_usd": record.cost_usd, + "tool_calls": record.tool_calls as i64, + }; + self.events + .insert_one(doc) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + + /// Query total cost in the last hour for an agent. + pub async fn query_hourly(&self, agent_id: AgentId) -> OpenFangResult { + let cutoff = + bson::DateTime::from_chrono(Utc::now() - chrono::Duration::hours(1)); + self.sum_cost(doc! { "agent_id": agent_id.0.to_string(), "timestamp": { "$gt": cutoff } }) + .await + } + + /// Query total cost today for an agent. + pub async fn query_daily(&self, agent_id: AgentId) -> OpenFangResult { + let today = Utc::now().date_naive().and_hms_opt(0, 0, 0).unwrap(); + let cutoff = bson::DateTime::from_chrono( + chrono::DateTime::::from_naive_utc_and_offset(today, Utc), + ); + self.sum_cost(doc! { "agent_id": agent_id.0.to_string(), "timestamp": { "$gte": cutoff } }) + .await + } + + /// Query total cost in the current calendar month for an agent. + pub async fn query_monthly(&self, agent_id: AgentId) -> OpenFangResult { + let now = Utc::now(); + let first_of_month = now.date_naive().with_day(1).unwrap().and_hms_opt(0, 0, 0).unwrap(); + let cutoff = bson::DateTime::from_chrono( + chrono::DateTime::::from_naive_utc_and_offset(first_of_month, Utc), + ); + self.sum_cost(doc! { "agent_id": agent_id.0.to_string(), "timestamp": { "$gte": cutoff } }) + .await + } + + /// Query total cost across all agents for the current hour. + pub async fn query_global_hourly(&self) -> OpenFangResult { + let cutoff = + bson::DateTime::from_chrono(Utc::now() - chrono::Duration::hours(1)); + self.sum_cost(doc! { "timestamp": { "$gt": cutoff } }).await + } + + /// Query total cost across all agents for the current calendar month. + pub async fn query_global_monthly(&self) -> OpenFangResult { + let now = Utc::now(); + let first_of_month = now.date_naive().with_day(1).unwrap().and_hms_opt(0, 0, 0).unwrap(); + let cutoff = bson::DateTime::from_chrono( + chrono::DateTime::::from_naive_utc_and_offset(first_of_month, Utc), + ); + self.sum_cost(doc! { "timestamp": { "$gte": cutoff } }).await + } + + /// Query usage summary, optionally filtered by agent. + pub async fn query_summary(&self, agent_id: Option) -> OpenFangResult { + let match_stage = match agent_id { + Some(aid) => doc! { "$match": { "agent_id": aid.0.to_string() } }, + None => doc! { "$match": {} }, + }; + let group_stage = doc! { + "$group": { + "_id": bson::Bson::Null, + "total_input_tokens": { "$sum": "$input_tokens" }, + "total_output_tokens": { "$sum": "$output_tokens" }, + "total_cost_usd": { "$sum": "$cost_usd" }, + "call_count": { "$sum": 1 }, + "total_tool_calls": { "$sum": "$tool_calls" }, + } + }; + + let mut cursor = self + .events + .aggregate(vec![match_stage, group_stage]) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + if let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + Ok(UsageSummary { + total_input_tokens: d.get_i64("total_input_tokens").unwrap_or(0) as u64, + total_output_tokens: d.get_i64("total_output_tokens").unwrap_or(0) as u64, + total_cost_usd: d.get_f64("total_cost_usd").unwrap_or(0.0), + call_count: d.get_i32("call_count").unwrap_or(0) as u64, + total_tool_calls: d.get_i64("total_tool_calls").unwrap_or(0) as u64, + }) + } else { + Ok(UsageSummary { + total_input_tokens: 0, + total_output_tokens: 0, + total_cost_usd: 0.0, + call_count: 0, + total_tool_calls: 0, + }) + } + } + + /// Query usage grouped by model. + pub async fn query_by_model(&self) -> OpenFangResult> { + let pipeline = vec![ + doc! { + "$group": { + "_id": "$model", + "total_cost_usd": { "$sum": "$cost_usd" }, + "total_input_tokens": { "$sum": "$input_tokens" }, + "total_output_tokens": { "$sum": "$output_tokens" }, + "call_count": { "$sum": 1 }, + } + }, + doc! { "$sort": { "total_cost_usd": -1 } }, + ]; + + let mut cursor = self + .events + .aggregate(pipeline) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut results = Vec::new(); + while let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + results.push(ModelUsage { + model: d.get_str("_id").unwrap_or("unknown").to_string(), + total_cost_usd: d.get_f64("total_cost_usd").unwrap_or(0.0), + total_input_tokens: d.get_i64("total_input_tokens").unwrap_or(0) as u64, + total_output_tokens: d.get_i64("total_output_tokens").unwrap_or(0) as u64, + call_count: d.get_i32("call_count").unwrap_or(0) as u64, + }); + } + Ok(results) + } + + /// Query daily usage breakdown for the last N days. + pub async fn query_daily_breakdown(&self, days: u32) -> OpenFangResult> { + let cutoff = bson::DateTime::from_chrono( + Utc::now() - chrono::Duration::days(days as i64), + ); + let pipeline = vec![ + doc! { "$match": { "timestamp": { "$gt": cutoff } } }, + doc! { + "$group": { + "_id": { "$dateToString": { "format": "%Y-%m-%d", "date": "$timestamp" } }, + "cost_usd": { "$sum": "$cost_usd" }, + "tokens": { "$sum": { "$add": ["$input_tokens", "$output_tokens"] } }, + "calls": { "$sum": 1 }, + } + }, + doc! { "$sort": { "_id": 1 } }, + ]; + + let mut cursor = self + .events + .aggregate(pipeline) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut results = Vec::new(); + while let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + results.push(DailyBreakdown { + date: d.get_str("_id").unwrap_or("").to_string(), + cost_usd: d.get_f64("cost_usd").unwrap_or(0.0), + tokens: d.get_i64("tokens").unwrap_or(0) as u64, + calls: d.get_i32("calls").unwrap_or(0) as u64, + }); + } + Ok(results) + } + + /// Query the timestamp of the earliest usage event. + pub async fn query_first_event_date(&self) -> OpenFangResult> { + let opts = mongodb::options::FindOptions::builder() + .sort(doc! { "timestamp": 1 }) + .limit(1) + .projection(doc! { "timestamp": 1 }) + .build(); + let mut cursor = self + .events + .find(doc! {}) + .with_options(opts) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + if let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + let ts = d + .get_datetime("timestamp") + .ok() + .map(|dt| dt.to_chrono().to_rfc3339()); + Ok(ts) + } else { + Ok(None) + } + } + + /// Query today's total cost across all agents. + pub async fn query_today_cost(&self) -> OpenFangResult { + let today = Utc::now().date_naive().and_hms_opt(0, 0, 0).unwrap(); + let cutoff = bson::DateTime::from_chrono( + chrono::DateTime::::from_naive_utc_and_offset(today, Utc), + ); + self.sum_cost(doc! { "timestamp": { "$gte": cutoff } }).await + } + + /// Delete usage events older than the given number of days. + pub async fn cleanup_old(&self, days: u32) -> OpenFangResult { + let cutoff = bson::DateTime::from_chrono( + Utc::now() - chrono::Duration::days(days as i64), + ); + let result = self + .events + .delete_many(doc! { "timestamp": { "$lt": cutoff } }) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(result.deleted_count as usize) + } + + /// Helper: sum cost_usd matching a filter using aggregation. + async fn sum_cost(&self, filter: bson::Document) -> OpenFangResult { + let pipeline = vec![ + doc! { "$match": filter }, + doc! { + "$group": { + "_id": bson::Bson::Null, + "total": { "$sum": "$cost_usd" }, + } + }, + ]; + let mut cursor = self + .events + .aggregate(pipeline) + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + if let Some(d) = cursor + .try_next() + .await + .map_err(|e| OpenFangError::Memory(e.to_string()))? + { + Ok(d.get_f64("total").unwrap_or(0.0)) + } else { + Ok(0.0) + } + } +} diff --git a/crates/openfang-memory/src/session.rs b/crates/openfang-memory/src/session.rs index a44035342..578fe7b31 100644 --- a/crates/openfang-memory/src/session.rs +++ b/crates/openfang-memory/src/session.rs @@ -535,20 +535,29 @@ impl SessionStore { session: &Session, sessions_dir: &Path, ) -> Result<(), std::io::Error> { - std::fs::create_dir_all(sessions_dir)?; - let path = sessions_dir.join(format!("{}.jsonl", session.id.0)); - let mut file = std::fs::File::create(&path)?; - let now = Utc::now().to_rfc3339(); + write_jsonl_mirror_impl(session, sessions_dir) + } +} - for msg in &session.messages { - let role_str = match msg.role { - Role::User => "user", - Role::Assistant => "assistant", - Role::System => "system", - }; +/// Shared implementation for writing JSONL mirror files (used by both SQLite and MongoDB session stores). +pub(crate) fn write_jsonl_mirror_impl( + session: &Session, + sessions_dir: &Path, +) -> Result<(), std::io::Error> { + std::fs::create_dir_all(sessions_dir)?; + let path = sessions_dir.join(format!("{}.jsonl", session.id.0)); + let mut file = std::fs::File::create(&path)?; + let now = Utc::now().to_rfc3339(); + + for msg in &session.messages { + let role_str = match msg.role { + Role::User => "user", + Role::Assistant => "assistant", + Role::System => "system", + }; - let mut text_parts: Vec = Vec::new(); - let mut tool_parts: Vec = Vec::new(); + let mut text_parts: Vec = Vec::new(); + let mut tool_parts: Vec = Vec::new(); match &msg.content { MessageContent::Text(t) => { @@ -594,27 +603,37 @@ impl SessionStore { } ContentBlock::Unknown => {} } + ContentBlock::Image { media_type, .. } => { + text_parts.push(format!("[image: {media_type}]")); + } + ContentBlock::Thinking { thinking } => { + text_parts.push(format!( + "[thinking: {}]", + openfang_types::truncate_str(thinking, 200) + )); + } + ContentBlock::Unknown => {} } } } - - let line = JsonlLine { - timestamp: now.clone(), - role: role_str.to_string(), - content: serde_json::Value::String(text_parts.join("\n")), - tool_use: if tool_parts.is_empty() { - None - } else { - Some(serde_json::Value::Array(tool_parts)) - }, - }; - - serde_json::to_writer(&mut file, &line).map_err(std::io::Error::other)?; - file.write_all(b"\n")?; } - Ok(()) + let line = JsonlLine { + timestamp: now.clone(), + role: role_str.to_string(), + content: serde_json::Value::String(text_parts.join("\n")), + tool_use: if tool_parts.is_empty() { + None + } else { + Some(serde_json::Value::Array(tool_parts)) + }, + }; + + serde_json::to_writer(&mut file, &line).map_err(std::io::Error::other)?; + file.write_all(b"\n")?; } + + Ok(()) } #[cfg(test)] diff --git a/crates/openfang-memory/src/substrate.rs b/crates/openfang-memory/src/substrate.rs index 34232dd35..3dab0281f 100644 --- a/crates/openfang-memory/src/substrate.rs +++ b/crates/openfang-memory/src/substrate.rs @@ -2,10 +2,12 @@ //! //! Composes the structured store, semantic store, knowledge store, //! session store, and consolidation engine behind a single async API. +//! Supports both SQLite and MongoDB backends via `BackendInner` dispatch. use crate::consolidation::ConsolidationEngine; use crate::knowledge::KnowledgeStore; use crate::migration::run_migrations; +use crate::mongo::MongoBackend; use crate::semantic::SemanticStore; use crate::session::{Session, SessionStore}; use crate::structured::StructuredStore; @@ -23,20 +25,33 @@ use std::collections::HashMap; use std::path::Path; use std::sync::{Arc, Mutex}; +/// Internal backend discriminator. +enum BackendInner { + Sqlite { + conn: Arc>, + structured: StructuredStore, + semantic: SemanticStore, + knowledge: KnowledgeStore, + sessions: SessionStore, + consolidation: ConsolidationEngine, + usage: UsageStore, + }, + Mongo(MongoBackend), +} + +/// Helper: run an async future from a sync context on the current tokio runtime. +fn block_on(f: F) -> F::Output { + tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(f)) +} + /// The unified memory substrate. Implements the `Memory` trait by delegating -/// to specialized stores backed by a shared SQLite connection. +/// to specialized stores backed by either SQLite or MongoDB. pub struct MemorySubstrate { - conn: Arc>, - structured: StructuredStore, - semantic: SemanticStore, - knowledge: KnowledgeStore, - sessions: SessionStore, - consolidation: ConsolidationEngine, - usage: UsageStore, + inner: BackendInner, } impl MemorySubstrate { - /// Open or create a memory substrate at the given database path. + /// Open or create a SQLite-backed memory substrate at the given database path. pub fn open(db_path: &Path, decay_rate: f32) -> OpenFangResult { let conn = Connection::open(db_path).map_err(|e| OpenFangError::Memory(e.to_string()))?; conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;") @@ -45,17 +60,54 @@ impl MemorySubstrate { let shared = Arc::new(Mutex::new(conn)); Ok(Self { - conn: Arc::clone(&shared), - structured: StructuredStore::new(Arc::clone(&shared)), - semantic: SemanticStore::new(Arc::clone(&shared)), - knowledge: KnowledgeStore::new(Arc::clone(&shared)), - sessions: SessionStore::new(Arc::clone(&shared)), - usage: UsageStore::new(Arc::clone(&shared)), - consolidation: ConsolidationEngine::new(shared, decay_rate), + inner: BackendInner::Sqlite { + conn: Arc::clone(&shared), + structured: StructuredStore::new(Arc::clone(&shared)), + semantic: SemanticStore::new(Arc::clone(&shared)), + knowledge: KnowledgeStore::new(Arc::clone(&shared)), + sessions: SessionStore::new(Arc::clone(&shared)), + usage: UsageStore::new(Arc::clone(&shared)), + consolidation: ConsolidationEngine::new(shared, decay_rate), + }, + }) + } + + /// Open a MongoDB-backed memory substrate. + pub async fn open_mongo( + mongo_url: &str, + db_name: &str, + decay_rate: f32, + ) -> OpenFangResult { + let client = mongodb::Client::with_uri_str(mongo_url) + .await + .map_err(|e| OpenFangError::Memory(format!("MongoDB connection failed: {e}")))?; + let db = client.database(db_name); + crate::mongo::indexes::ensure_indexes(&db).await?; + Ok(Self { + inner: BackendInner::Mongo(MongoBackend::new(db, decay_rate)), }) } - /// Create an in-memory substrate (for testing). + /// Open a memory substrate from configuration — dispatches to SQLite or MongoDB. + pub async fn open_with_config( + config: &openfang_types::config::MemoryConfig, + ) -> OpenFangResult { + match config.backend.as_str() { + "mongodb" => { + Self::open_mongo(&config.mongo_url, &config.mongo_db_name, config.decay_rate) + .await + } + _ => { + let db_path = config + .sqlite_path + .clone() + .expect("sqlite_path must be set when backend is sqlite"); + Self::open(&db_path, config.decay_rate) + } + } + } + + /// Create an in-memory SQLite substrate (for testing). pub fn open_in_memory(decay_rate: f32) -> OpenFangResult { let conn = Connection::open_in_memory().map_err(|e| OpenFangError::Memory(e.to_string()))?; @@ -63,242 +115,361 @@ impl MemorySubstrate { let shared = Arc::new(Mutex::new(conn)); Ok(Self { - conn: Arc::clone(&shared), - structured: StructuredStore::new(Arc::clone(&shared)), - semantic: SemanticStore::new(Arc::clone(&shared)), - knowledge: KnowledgeStore::new(Arc::clone(&shared)), - sessions: SessionStore::new(Arc::clone(&shared)), - usage: UsageStore::new(Arc::clone(&shared)), - consolidation: ConsolidationEngine::new(shared, decay_rate), + inner: BackendInner::Sqlite { + conn: Arc::clone(&shared), + structured: StructuredStore::new(Arc::clone(&shared)), + semantic: SemanticStore::new(Arc::clone(&shared)), + knowledge: KnowledgeStore::new(Arc::clone(&shared)), + sessions: SessionStore::new(Arc::clone(&shared)), + usage: UsageStore::new(Arc::clone(&shared)), + consolidation: ConsolidationEngine::new(shared, decay_rate), + }, }) } - /// Get a reference to the usage store. - pub fn usage(&self) -> &UsageStore { - &self.usage + /// Get a reference to the usage store (SQLite only). + pub fn usage(&self) -> Option<&UsageStore> { + match &self.inner { + BackendInner::Sqlite { usage, .. } => Some(usage), + BackendInner::Mongo(_) => None, + } + } + + /// Get the shared database connection (SQLite only, for external UsageStore/AuditLog). + pub fn usage_conn(&self) -> Option>> { + match &self.inner { + BackendInner::Sqlite { conn, .. } => Some(Arc::clone(conn)), + BackendInner::Mongo(_) => None, + } } - /// Get the shared database connection (for constructing stores from outside). - pub fn usage_conn(&self) -> Arc> { - Arc::clone(&self.conn) + /// Get the MongoDB database handle (MongoDB only, for external components). + pub fn mongo_db(&self) -> Option { + match &self.inner { + BackendInner::Sqlite { .. } => None, + BackendInner::Mongo(m) => Some(m.db.clone()), + } } - /// Save an agent entry to persistent storage. + /// Returns true if using the MongoDB backend. + pub fn is_mongo(&self) -> bool { + matches!(&self.inner, BackendInner::Mongo(_)) + } + + /// Create a backend-appropriate `UsageStore` instance. + /// + /// Returns a `UsageStore` backed by either SQLite or MongoDB depending + /// on which backend is active. + pub fn create_usage_store(&self) -> UsageStore { + match &self.inner { + BackendInner::Sqlite { conn, .. } => UsageStore::new(Arc::clone(conn)), + BackendInner::Mongo(m) => UsageStore::from_mongo(m.usage.clone()), + } + } + + // ----------------------------------------------------------------- + // Agent CRUD + // ----------------------------------------------------------------- + pub fn save_agent(&self, entry: &AgentEntry) -> OpenFangResult<()> { - self.structured.save_agent(entry) + match &self.inner { + BackendInner::Sqlite { structured, .. } => structured.save_agent(entry), + BackendInner::Mongo(m) => block_on(m.structured.save_agent(entry)), + } } - /// Load an agent entry from persistent storage. pub fn load_agent(&self, agent_id: AgentId) -> OpenFangResult> { - self.structured.load_agent(agent_id) + match &self.inner { + BackendInner::Sqlite { structured, .. } => structured.load_agent(agent_id), + BackendInner::Mongo(m) => block_on(m.structured.load_agent(agent_id)), + } } - /// Remove an agent from persistent storage and cascade-delete sessions. pub fn remove_agent(&self, agent_id: AgentId) -> OpenFangResult<()> { - // Delete associated sessions first - let _ = self.sessions.delete_agent_sessions(agent_id); - self.structured.remove_agent(agent_id) + match &self.inner { + BackendInner::Sqlite { + structured, + sessions, + .. + } => { + let _ = sessions.delete_agent_sessions(agent_id); + structured.remove_agent(agent_id) + } + BackendInner::Mongo(m) => block_on(async { + let _ = m.sessions.delete_agent_sessions(agent_id).await; + m.structured.remove_agent(agent_id).await + }), + } } - /// Load all agent entries from persistent storage. pub fn load_all_agents(&self) -> OpenFangResult> { - self.structured.load_all_agents() + match &self.inner { + BackendInner::Sqlite { structured, .. } => structured.load_all_agents(), + BackendInner::Mongo(m) => block_on(m.structured.load_all_agents()), + } } - /// List all saved agents. pub fn list_agents(&self) -> OpenFangResult> { - self.structured.list_agents() + match &self.inner { + BackendInner::Sqlite { structured, .. } => structured.list_agents(), + BackendInner::Mongo(m) => block_on(m.structured.list_agents()), + } } - /// Synchronous get from the structured store (for kernel handle use). + // ----------------------------------------------------------------- + // Structured KV + // ----------------------------------------------------------------- + pub fn structured_get( &self, agent_id: AgentId, key: &str, ) -> OpenFangResult> { - self.structured.get(agent_id, key) + match &self.inner { + BackendInner::Sqlite { structured, .. } => structured.get(agent_id, key), + BackendInner::Mongo(m) => block_on(m.structured.get(agent_id, key)), + } } - /// List all KV pairs for an agent. pub fn list_kv(&self, agent_id: AgentId) -> OpenFangResult> { - self.structured.list_kv(agent_id) + match &self.inner { + BackendInner::Sqlite { structured, .. } => structured.list_kv(agent_id), + BackendInner::Mongo(m) => block_on(m.structured.list_kv(agent_id)), + } } - /// Delete a KV entry for an agent. pub fn structured_delete(&self, agent_id: AgentId, key: &str) -> OpenFangResult<()> { - self.structured.delete(agent_id, key) + match &self.inner { + BackendInner::Sqlite { structured, .. } => structured.delete(agent_id, key), + BackendInner::Mongo(m) => block_on(m.structured.delete(agent_id, key)), + } } - /// Synchronous set in the structured store (for kernel handle use). pub fn structured_set( &self, agent_id: AgentId, key: &str, value: serde_json::Value, ) -> OpenFangResult<()> { - self.structured.set(agent_id, key, value) + match &self.inner { + BackendInner::Sqlite { structured, .. } => structured.set(agent_id, key, value), + BackendInner::Mongo(m) => block_on(m.structured.set(agent_id, key, value)), + } } - /// Get a session by ID. + // ----------------------------------------------------------------- + // Sessions + // ----------------------------------------------------------------- + pub fn get_session(&self, session_id: SessionId) -> OpenFangResult> { - self.sessions.get_session(session_id) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => sessions.get_session(session_id), + BackendInner::Mongo(m) => block_on(m.sessions.get_session(session_id)), + } } - /// Save a session. pub fn save_session(&self, session: &Session) -> OpenFangResult<()> { - self.sessions.save_session(session) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => sessions.save_session(session), + BackendInner::Mongo(m) => block_on(m.sessions.save_session(session)), + } } - /// Save a session asynchronously — runs the SQLite write in a blocking - /// thread so the tokio runtime stays responsive. pub async fn save_session_async(&self, session: &Session) -> OpenFangResult<()> { - let sessions = self.sessions.clone(); - let session = session.clone(); - tokio::task::spawn_blocking(move || sessions.save_session(&session)) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { sessions, .. } => { + let store = sessions.clone(); + let session = session.clone(); + tokio::task::spawn_blocking(move || store.save_session(&session)) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => m.sessions.save_session(session).await, + } } - /// Create a new empty session for an agent. pub fn create_session(&self, agent_id: AgentId) -> OpenFangResult { - self.sessions.create_session(agent_id) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => sessions.create_session(agent_id), + BackendInner::Mongo(m) => block_on(m.sessions.create_session(agent_id)), + } } - /// List all sessions with metadata. pub fn list_sessions(&self) -> OpenFangResult> { - self.sessions.list_sessions() + match &self.inner { + BackendInner::Sqlite { sessions, .. } => sessions.list_sessions(), + BackendInner::Mongo(m) => block_on(m.sessions.list_sessions()), + } } - /// Delete a session by ID. pub fn delete_session(&self, session_id: SessionId) -> OpenFangResult<()> { - self.sessions.delete_session(session_id) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => sessions.delete_session(session_id), + BackendInner::Mongo(m) => block_on(m.sessions.delete_session(session_id)), + } } - /// Delete all sessions belonging to an agent. pub fn delete_agent_sessions(&self, agent_id: AgentId) -> OpenFangResult<()> { - self.sessions.delete_agent_sessions(agent_id) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => sessions.delete_agent_sessions(agent_id), + BackendInner::Mongo(m) => block_on(m.sessions.delete_agent_sessions(agent_id)), + } } - /// Delete the canonical (cross-channel) session for an agent. pub fn delete_canonical_session(&self, agent_id: AgentId) -> OpenFangResult<()> { - self.sessions.delete_canonical_session(agent_id) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => sessions.delete_canonical_session(agent_id), + BackendInner::Mongo(m) => block_on(m.sessions.delete_canonical_session(agent_id)), + } } - /// Set or clear a session label. pub fn set_session_label( &self, session_id: SessionId, label: Option<&str>, ) -> OpenFangResult<()> { - self.sessions.set_session_label(session_id, label) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => { + sessions.set_session_label(session_id, label) + } + BackendInner::Mongo(m) => block_on(m.sessions.set_session_label(session_id, label)), + } } - /// Find a session by label for a given agent. pub fn find_session_by_label( &self, agent_id: AgentId, label: &str, ) -> OpenFangResult> { - self.sessions.find_session_by_label(agent_id, label) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => { + sessions.find_session_by_label(agent_id, label) + } + BackendInner::Mongo(m) => { + block_on(m.sessions.find_session_by_label(agent_id, label)) + } + } } - /// List all sessions for a specific agent. pub fn list_agent_sessions(&self, agent_id: AgentId) -> OpenFangResult> { - self.sessions.list_agent_sessions(agent_id) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => sessions.list_agent_sessions(agent_id), + BackendInner::Mongo(m) => block_on(m.sessions.list_agent_sessions(agent_id)), + } } - /// Create a new session with an optional label. pub fn create_session_with_label( &self, agent_id: AgentId, label: Option<&str>, ) -> OpenFangResult { - self.sessions.create_session_with_label(agent_id, label) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => { + sessions.create_session_with_label(agent_id, label) + } + BackendInner::Mongo(m) => { + block_on(m.sessions.create_session_with_label(agent_id, label)) + } + } } - /// Load canonical session context for cross-channel memory. - /// - /// Returns the compacted summary (if any) and recent messages from the - /// agent's persistent canonical session. pub fn canonical_context( &self, agent_id: AgentId, window_size: Option, ) -> OpenFangResult<(Option, Vec)> { - self.sessions.canonical_context(agent_id, window_size) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => { + sessions.canonical_context(agent_id, window_size) + } + BackendInner::Mongo(m) => { + block_on(m.sessions.canonical_context(agent_id, window_size)) + } + } } - /// Store an LLM-generated summary, replacing older messages with the kept subset. - /// - /// Used by the compactor to replace text-truncation compaction with an - /// LLM-generated summary of older conversation history. pub fn store_llm_summary( &self, agent_id: AgentId, summary: &str, kept_messages: Vec, ) -> OpenFangResult<()> { - self.sessions - .store_llm_summary(agent_id, summary, kept_messages) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => { + sessions.store_llm_summary(agent_id, summary, kept_messages) + } + BackendInner::Mongo(m) => { + block_on(m.sessions.store_llm_summary(agent_id, summary, kept_messages)) + } + } } - /// Write a human-readable JSONL mirror of a session to disk. - /// - /// Best-effort — errors are returned but should be logged, - /// never affecting the primary SQLite store. pub fn write_jsonl_mirror( &self, session: &Session, sessions_dir: &Path, ) -> Result<(), std::io::Error> { - self.sessions.write_jsonl_mirror(session, sessions_dir) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => { + sessions.write_jsonl_mirror(session, sessions_dir) + } + BackendInner::Mongo(m) => m.sessions.write_jsonl_mirror(session, sessions_dir), + } } - /// Append messages to the agent's canonical session for cross-channel persistence. pub fn append_canonical( &self, agent_id: AgentId, messages: &[openfang_types::message::Message], compaction_threshold: Option, ) -> OpenFangResult<()> { - self.sessions - .append_canonical(agent_id, messages, compaction_threshold)?; - Ok(()) + match &self.inner { + BackendInner::Sqlite { sessions, .. } => { + sessions.append_canonical(agent_id, messages, compaction_threshold)?; + Ok(()) + } + BackendInner::Mongo(m) => { + block_on(m.sessions.append_canonical(agent_id, messages, compaction_threshold))?; + Ok(()) + } + } } // ----------------------------------------------------------------- // Paired devices persistence // ----------------------------------------------------------------- - /// Load all paired devices from the database. pub fn load_paired_devices(&self) -> OpenFangResult> { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - let mut stmt = conn.prepare( - "SELECT device_id, display_name, platform, paired_at, last_seen, push_token FROM paired_devices" - ).map_err(|e| OpenFangError::Memory(e.to_string()))?; - let rows = stmt - .query_map([], |row| { - Ok(serde_json::json!({ - "device_id": row.get::<_, String>(0)?, - "display_name": row.get::<_, String>(1)?, - "platform": row.get::<_, String>(2)?, - "paired_at": row.get::<_, String>(3)?, - "last_seen": row.get::<_, String>(4)?, - "push_token": row.get::<_, Option>(5)?, - })) - }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - let mut devices = Vec::new(); - for row in rows { - devices.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + match &self.inner { + BackendInner::Sqlite { conn, .. } => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + let mut stmt = conn.prepare( + "SELECT device_id, display_name, platform, paired_at, last_seen, push_token FROM paired_devices" + ).map_err(|e| OpenFangError::Memory(e.to_string()))?; + let rows = stmt + .query_map([], |row| { + Ok(serde_json::json!({ + "device_id": row.get::<_, String>(0)?, + "display_name": row.get::<_, String>(1)?, + "platform": row.get::<_, String>(2)?, + "paired_at": row.get::<_, String>(3)?, + "last_seen": row.get::<_, String>(4)?, + "push_token": row.get::<_, Option>(5)?, + })) + }) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + let mut devices = Vec::new(); + for row in rows { + devices.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + } + Ok(devices) + } + BackendInner::Mongo(m) => block_on(m.load_paired_devices()), } - Ok(devices) } - /// Save a paired device to the database (insert or replace). pub fn save_paired_device( &self, device_id: &str, @@ -308,36 +479,44 @@ impl MemorySubstrate { last_seen: &str, push_token: Option<&str>, ) -> OpenFangResult<()> { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - conn.execute( - "INSERT OR REPLACE INTO paired_devices (device_id, display_name, platform, paired_at, last_seen, push_token) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - rusqlite::params![device_id, display_name, platform, paired_at, last_seen, push_token], - ).map_err(|e| OpenFangError::Memory(e.to_string()))?; - Ok(()) + match &self.inner { + BackendInner::Sqlite { conn, .. } => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + conn.execute( + "INSERT OR REPLACE INTO paired_devices (device_id, display_name, platform, paired_at, last_seen, push_token) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + rusqlite::params![device_id, display_name, platform, paired_at, last_seen, push_token], + ).map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + BackendInner::Mongo(m) => block_on( + m.save_paired_device(device_id, display_name, platform, paired_at, last_seen, push_token), + ), + } } - /// Remove a paired device from the database. pub fn remove_paired_device(&self, device_id: &str) -> OpenFangResult<()> { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - conn.execute( - "DELETE FROM paired_devices WHERE device_id = ?1", - rusqlite::params![device_id], - ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - Ok(()) + match &self.inner { + BackendInner::Sqlite { conn, .. } => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + conn.execute( + "DELETE FROM paired_devices WHERE device_id = ?1", + rusqlite::params![device_id], + ) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + BackendInner::Mongo(m) => block_on(m.remove_paired_device(device_id)), + } } // ----------------------------------------------------------------- // Embedding-aware memory operations // ----------------------------------------------------------------- - /// Store a memory with an embedding vector. pub fn remember_with_embedding( &self, agent_id: AgentId, @@ -347,11 +526,16 @@ impl MemorySubstrate { metadata: HashMap, embedding: Option<&[f32]>, ) -> OpenFangResult { - self.semantic - .remember_with_embedding(agent_id, content, source, scope, metadata, embedding) + match &self.inner { + BackendInner::Sqlite { semantic, .. } => { + semantic.remember_with_embedding(agent_id, content, source, scope, metadata, embedding) + } + BackendInner::Mongo(m) => { + block_on(m.semantic.remember_with_embedding(agent_id, content, source, scope, metadata, embedding)) + } + } } - /// Recall memories using vector similarity when a query embedding is provided. pub fn recall_with_embedding( &self, query: &str, @@ -359,16 +543,23 @@ impl MemorySubstrate { filter: Option, query_embedding: Option<&[f32]>, ) -> OpenFangResult> { - self.semantic - .recall_with_embedding(query, limit, filter, query_embedding) + match &self.inner { + BackendInner::Sqlite { semantic, .. } => { + semantic.recall_with_embedding(query, limit, filter, query_embedding) + } + BackendInner::Mongo(m) => { + block_on(m.semantic.recall_with_embedding(query, limit, filter, query_embedding)) + } + } } - /// Update the embedding for an existing memory. pub fn update_embedding(&self, id: MemoryId, embedding: &[f32]) -> OpenFangResult<()> { - self.semantic.update_embedding(id, embedding) + match &self.inner { + BackendInner::Sqlite { semantic, .. } => semantic.update_embedding(id, embedding), + BackendInner::Mongo(m) => block_on(m.semantic.update_embedding(id, embedding)), + } } - /// Async wrapper for `recall_with_embedding` — runs in a blocking thread. pub async fn recall_with_embedding_async( &self, query: &str, @@ -376,17 +567,25 @@ impl MemorySubstrate { filter: Option, query_embedding: Option<&[f32]>, ) -> OpenFangResult> { - let store = self.semantic.clone(); - let query = query.to_string(); - let embedding_owned = query_embedding.map(|e| e.to_vec()); - tokio::task::spawn_blocking(move || { - store.recall_with_embedding(&query, limit, filter, embedding_owned.as_deref()) - }) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { semantic, .. } => { + let store = semantic.clone(); + let query = query.to_string(); + let embedding_owned = query_embedding.map(|e| e.to_vec()); + tokio::task::spawn_blocking(move || { + store.recall_with_embedding(&query, limit, filter, embedding_owned.as_deref()) + }) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => { + m.semantic + .recall_with_embedding(query, limit, filter, query_embedding) + .await + } + } } - /// Async wrapper for `remember_with_embedding` — runs in a blocking thread. pub async fn remember_with_embedding_async( &self, agent_id: AgentId, @@ -396,29 +595,37 @@ impl MemorySubstrate { metadata: HashMap, embedding: Option<&[f32]>, ) -> OpenFangResult { - let store = self.semantic.clone(); - let content = content.to_string(); - let scope = scope.to_string(); - let embedding_owned = embedding.map(|e| e.to_vec()); - tokio::task::spawn_blocking(move || { - store.remember_with_embedding( - agent_id, - &content, - source, - &scope, - metadata, - embedding_owned.as_deref(), - ) - }) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { semantic, .. } => { + let store = semantic.clone(); + let content = content.to_string(); + let scope = scope.to_string(); + let embedding_owned = embedding.map(|e| e.to_vec()); + tokio::task::spawn_blocking(move || { + store.remember_with_embedding( + agent_id, + &content, + source, + &scope, + metadata, + embedding_owned.as_deref(), + ) + }) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => { + m.semantic + .remember_with_embedding(agent_id, content, source, scope, metadata, embedding) + .await + } + } } // ----------------------------------------------------------------- // Task queue operations // ----------------------------------------------------------------- - /// Post a new task to the shared queue. Returns the task ID. pub async fn task_post( &self, title: &str, @@ -426,156 +633,178 @@ impl MemorySubstrate { assigned_to: Option<&str>, created_by: Option<&str>, ) -> OpenFangResult { - let conn = Arc::clone(&self.conn); - let title = title.to_string(); - let description = description.to_string(); - let assigned_to = assigned_to.unwrap_or("").to_string(); - let created_by = created_by.unwrap_or("").to_string(); - - tokio::task::spawn_blocking(move || { - let id = uuid::Uuid::new_v4().to_string(); - let now = chrono::Utc::now().to_rfc3339(); - let db = conn.lock().map_err(|e| OpenFangError::Internal(e.to_string()))?; - db.execute( - "INSERT INTO task_queue (id, agent_id, task_type, payload, status, priority, created_at, title, description, assigned_to, created_by) - VALUES (?1, ?2, ?3, ?4, 'pending', 0, ?5, ?6, ?7, ?8, ?9)", - rusqlite::params![id, &created_by, &title, b"", now, title, description, assigned_to, created_by], - ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - Ok(id) - }) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { conn, .. } => { + let conn = Arc::clone(conn); + let title = title.to_string(); + let description = description.to_string(); + let assigned_to = assigned_to.unwrap_or("").to_string(); + let created_by = created_by.unwrap_or("").to_string(); + + tokio::task::spawn_blocking(move || { + let id = uuid::Uuid::new_v4().to_string(); + let now = chrono::Utc::now().to_rfc3339(); + let db = conn.lock().map_err(|e| OpenFangError::Internal(e.to_string()))?; + db.execute( + "INSERT INTO task_queue (id, agent_id, task_type, payload, status, priority, created_at, title, description, assigned_to, created_by) + VALUES (?1, ?2, ?3, ?4, 'pending', 0, ?5, ?6, ?7, ?8, ?9)", + rusqlite::params![id, &created_by, &title, b"", now, title, description, assigned_to, created_by], + ) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(id) + }) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => { + m.task_post(title, description, assigned_to, created_by).await + } + } } - /// Claim the next pending task (optionally for a specific assignee). Returns task JSON or None. pub async fn task_claim(&self, agent_id: &str) -> OpenFangResult> { - let conn = Arc::clone(&self.conn); - let agent_id = agent_id.to_string(); - - tokio::task::spawn_blocking(move || { - let db = conn.lock().map_err(|e| OpenFangError::Internal(e.to_string()))?; - // Find first pending task assigned to this agent, or any unassigned pending task - let mut stmt = db.prepare( - "SELECT id, title, description, assigned_to, created_by, created_at - FROM task_queue - WHERE status = 'pending' AND (assigned_to = ?1 OR assigned_to = '') - ORDER BY priority DESC, created_at ASC - LIMIT 1" - ).map_err(|e| OpenFangError::Memory(e.to_string()))?; - - let result = stmt.query_row(rusqlite::params![agent_id], |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, - )) - }); - - match result { - Ok((id, title, description, assigned, created_by, created_at)) => { - // Update status to in_progress - db.execute( - "UPDATE task_queue SET status = 'in_progress', assigned_to = ?2 WHERE id = ?1", - rusqlite::params![id, agent_id], + match &self.inner { + BackendInner::Sqlite { conn, .. } => { + let conn = Arc::clone(conn); + let agent_id = agent_id.to_string(); + + tokio::task::spawn_blocking(move || { + let db = conn.lock().map_err(|e| OpenFangError::Internal(e.to_string()))?; + let mut stmt = db.prepare( + "SELECT id, title, description, assigned_to, created_by, created_at + FROM task_queue + WHERE status = 'pending' AND (assigned_to = ?1 OR assigned_to = '') + ORDER BY priority DESC, created_at ASC + LIMIT 1" ).map_err(|e| OpenFangError::Memory(e.to_string()))?; - Ok(Some(serde_json::json!({ - "id": id, - "title": title, - "description": description, - "status": "in_progress", - "assigned_to": if assigned.is_empty() { &agent_id } else { &assigned }, - "created_by": created_by, - "created_at": created_at, - }))) - } - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(OpenFangError::Memory(e.to_string())), + let result = stmt.query_row(rusqlite::params![agent_id], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, String>(4)?, + row.get::<_, String>(5)?, + )) + }); + + match result { + Ok((id, title, description, assigned, created_by, created_at)) => { + db.execute( + "UPDATE task_queue SET status = 'in_progress', assigned_to = ?2 WHERE id = ?1", + rusqlite::params![id, agent_id], + ).map_err(|e| OpenFangError::Memory(e.to_string()))?; + + Ok(Some(serde_json::json!({ + "id": id, + "title": title, + "description": description, + "status": "in_progress", + "assigned_to": if assigned.is_empty() { &agent_id } else { &assigned }, + "created_by": created_by, + "created_at": created_at, + }))) + } + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(OpenFangError::Memory(e.to_string())), + } + }) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? } - }) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + BackendInner::Mongo(m) => m.task_claim(agent_id).await, + } } - /// Mark a task as completed with a result string. pub async fn task_complete(&self, task_id: &str, result: &str) -> OpenFangResult<()> { - let conn = Arc::clone(&self.conn); - let task_id = task_id.to_string(); - let result = result.to_string(); - - tokio::task::spawn_blocking(move || { - let now = chrono::Utc::now().to_rfc3339(); - let db = conn.lock().map_err(|e| OpenFangError::Internal(e.to_string()))?; - let rows = db.execute( - "UPDATE task_queue SET status = 'completed', result = ?2, completed_at = ?3 WHERE id = ?1", - rusqlite::params![task_id, result, now], - ).map_err(|e| OpenFangError::Memory(e.to_string()))?; - if rows == 0 { - return Err(OpenFangError::Internal(format!("Task not found: {task_id}"))); - } - Ok(()) - }) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { conn, .. } => { + let conn = Arc::clone(conn); + let task_id = task_id.to_string(); + let result = result.to_string(); + + tokio::task::spawn_blocking(move || { + let now = chrono::Utc::now().to_rfc3339(); + let db = conn.lock().map_err(|e| OpenFangError::Internal(e.to_string()))?; + let rows = db.execute( + "UPDATE task_queue SET status = 'completed', result = ?2, completed_at = ?3 WHERE id = ?1", + rusqlite::params![task_id, result, now], + ).map_err(|e| OpenFangError::Memory(e.to_string()))?; + if rows == 0 { + return Err(OpenFangError::Internal(format!("Task not found: {task_id}"))); + } + Ok(()) + }) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => m.task_complete(task_id, result).await, + } } - /// List tasks, optionally filtered by status. pub async fn task_list(&self, status: Option<&str>) -> OpenFangResult> { - let conn = Arc::clone(&self.conn); - let status = status.map(|s| s.to_string()); - - tokio::task::spawn_blocking(move || { - let db = conn.lock().map_err(|e| OpenFangError::Internal(e.to_string()))?; - let (sql, params): (&str, Vec>) = match &status { - Some(s) => ( - "SELECT id, title, description, status, assigned_to, created_by, created_at, completed_at, result FROM task_queue WHERE status = ?1 ORDER BY created_at DESC", - vec![Box::new(s.clone())], - ), - None => ( - "SELECT id, title, description, status, assigned_to, created_by, created_at, completed_at, result FROM task_queue ORDER BY created_at DESC", - vec![], - ), - }; - - let mut stmt = db.prepare(sql).map_err(|e| OpenFangError::Memory(e.to_string()))?; - let params_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect(); - let rows = stmt.query_map(params_refs.as_slice(), |row| { - Ok(serde_json::json!({ - "id": row.get::<_, String>(0)?, - "title": row.get::<_, String>(1).unwrap_or_default(), - "description": row.get::<_, String>(2).unwrap_or_default(), - "status": row.get::<_, String>(3)?, - "assigned_to": row.get::<_, String>(4).unwrap_or_default(), - "created_by": row.get::<_, String>(5).unwrap_or_default(), - "created_at": row.get::<_, String>(6).unwrap_or_default(), - "completed_at": row.get::<_, Option>(7).unwrap_or(None), - "result": row.get::<_, Option>(8).unwrap_or(None), - })) - }).map_err(|e| OpenFangError::Memory(e.to_string()))?; - - let mut tasks = Vec::new(); - for row in rows { - tasks.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); - } - Ok(tasks) - }) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { conn, .. } => { + let conn = Arc::clone(conn); + let status = status.map(|s| s.to_string()); + + tokio::task::spawn_blocking(move || { + let db = conn.lock().map_err(|e| OpenFangError::Internal(e.to_string()))?; + let (sql, params): (&str, Vec>) = match &status { + Some(s) => ( + "SELECT id, title, description, status, assigned_to, created_by, created_at, completed_at, result FROM task_queue WHERE status = ?1 ORDER BY created_at DESC", + vec![Box::new(s.clone())], + ), + None => ( + "SELECT id, title, description, status, assigned_to, created_by, created_at, completed_at, result FROM task_queue ORDER BY created_at DESC", + vec![], + ), + }; + + let mut stmt = db.prepare(sql).map_err(|e| OpenFangError::Memory(e.to_string()))?; + let params_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect(); + let rows = stmt.query_map(params_refs.as_slice(), |row| { + Ok(serde_json::json!({ + "id": row.get::<_, String>(0)?, + "title": row.get::<_, String>(1).unwrap_or_default(), + "description": row.get::<_, String>(2).unwrap_or_default(), + "status": row.get::<_, String>(3)?, + "assigned_to": row.get::<_, String>(4).unwrap_or_default(), + "created_by": row.get::<_, String>(5).unwrap_or_default(), + "created_at": row.get::<_, String>(6).unwrap_or_default(), + "completed_at": row.get::<_, Option>(7).unwrap_or(None), + "result": row.get::<_, Option>(8).unwrap_or(None), + })) + }).map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut tasks = Vec::new(); + for row in rows { + tasks.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + } + Ok(tasks) + }) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => m.task_list(status).await, + } } } #[async_trait] impl Memory for MemorySubstrate { async fn get(&self, agent_id: AgentId, key: &str) -> OpenFangResult> { - let store = self.structured.clone(); - let key = key.to_string(); - tokio::task::spawn_blocking(move || store.get(agent_id, &key)) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { structured, .. } => { + let store = structured.clone(); + let key = key.to_string(); + tokio::task::spawn_blocking(move || store.get(agent_id, &key)) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => m.structured.get(agent_id, key).await, + } } async fn set( @@ -584,19 +813,29 @@ impl Memory for MemorySubstrate { key: &str, value: serde_json::Value, ) -> OpenFangResult<()> { - let store = self.structured.clone(); - let key = key.to_string(); - tokio::task::spawn_blocking(move || store.set(agent_id, &key, value)) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { structured, .. } => { + let store = structured.clone(); + let key = key.to_string(); + tokio::task::spawn_blocking(move || store.set(agent_id, &key, value)) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => m.structured.set(agent_id, key, value).await, + } } async fn delete(&self, agent_id: AgentId, key: &str) -> OpenFangResult<()> { - let store = self.structured.clone(); - let key = key.to_string(); - tokio::task::spawn_blocking(move || store.delete(agent_id, &key)) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { structured, .. } => { + let store = structured.clone(); + let key = key.to_string(); + tokio::task::spawn_blocking(move || store.delete(agent_id, &key)) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => m.structured.delete(agent_id, key).await, + } } async fn remember( @@ -607,14 +846,23 @@ impl Memory for MemorySubstrate { scope: &str, metadata: HashMap, ) -> OpenFangResult { - let store = self.semantic.clone(); - let content = content.to_string(); - let scope = scope.to_string(); - tokio::task::spawn_blocking(move || { - store.remember(agent_id, &content, source, &scope, metadata) - }) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { semantic, .. } => { + let store = semantic.clone(); + let content = content.to_string(); + let scope = scope.to_string(); + tokio::task::spawn_blocking(move || { + store.remember(agent_id, &content, source, &scope, metadata) + }) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => { + m.semantic + .remember(agent_id, content, source, scope, metadata) + .await + } + } } async fn recall( @@ -623,46 +871,76 @@ impl Memory for MemorySubstrate { limit: usize, filter: Option, ) -> OpenFangResult> { - let store = self.semantic.clone(); - let query = query.to_string(); - tokio::task::spawn_blocking(move || store.recall(&query, limit, filter)) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { semantic, .. } => { + let store = semantic.clone(); + let query = query.to_string(); + tokio::task::spawn_blocking(move || store.recall(&query, limit, filter)) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => m.semantic.recall(query, limit, filter).await, + } } async fn forget(&self, id: MemoryId) -> OpenFangResult<()> { - let store = self.semantic.clone(); - tokio::task::spawn_blocking(move || store.forget(id)) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { semantic, .. } => { + let store = semantic.clone(); + tokio::task::spawn_blocking(move || store.forget(id)) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => m.semantic.forget(id).await, + } } async fn add_entity(&self, entity: Entity) -> OpenFangResult { - let store = self.knowledge.clone(); - tokio::task::spawn_blocking(move || store.add_entity(entity)) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { knowledge, .. } => { + let store = knowledge.clone(); + tokio::task::spawn_blocking(move || store.add_entity(entity)) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => m.knowledge.add_entity(entity).await, + } } async fn add_relation(&self, relation: Relation) -> OpenFangResult { - let store = self.knowledge.clone(); - tokio::task::spawn_blocking(move || store.add_relation(relation)) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { knowledge, .. } => { + let store = knowledge.clone(); + tokio::task::spawn_blocking(move || store.add_relation(relation)) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => m.knowledge.add_relation(relation).await, + } } async fn query_graph(&self, pattern: GraphPattern) -> OpenFangResult> { - let store = self.knowledge.clone(); - tokio::task::spawn_blocking(move || store.query_graph(pattern)) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { knowledge, .. } => { + let store = knowledge.clone(); + tokio::task::spawn_blocking(move || store.query_graph(pattern)) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => m.knowledge.query_graph(pattern).await, + } } async fn consolidate(&self) -> OpenFangResult { - let engine = self.consolidation.clone(); - tokio::task::spawn_blocking(move || engine.consolidate()) - .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + match &self.inner { + BackendInner::Sqlite { consolidation, .. } => { + let engine = consolidation.clone(); + tokio::task::spawn_blocking(move || engine.consolidate()) + .await + .map_err(|e| OpenFangError::Internal(e.to_string()))? + } + BackendInner::Mongo(m) => m.consolidation.consolidate().await, + } } async fn export(&self, format: ExportFormat) -> OpenFangResult> { @@ -675,7 +953,7 @@ impl Memory for MemorySubstrate { entities_imported: 0, relations_imported: 0, memories_imported: 0, - errors: vec!["Import not yet implemented in Phase 1".to_string()], + errors: vec!["Import not yet implemented".to_string()], }) } } @@ -748,20 +1026,17 @@ mod tests { .await .unwrap(); - // Claim the task let claimed = substrate.task_claim("auditor").await.unwrap(); assert!(claimed.is_some()); let claimed = claimed.unwrap(); assert_eq!(claimed["id"], task_id); assert_eq!(claimed["status"], "in_progress"); - // Complete the task substrate .task_complete(&task_id, "No vulnerabilities found") .await .unwrap(); - // Verify it shows as completed let tasks = substrate.task_list(Some("completed")).await.unwrap(); assert_eq!(tasks.len(), 1); assert_eq!(tasks[0]["result"], "No vulnerabilities found"); diff --git a/crates/openfang-memory/src/usage.rs b/crates/openfang-memory/src/usage.rs index 277fef438..5c6b1d8bc 100644 --- a/crates/openfang-memory/src/usage.rs +++ b/crates/openfang-memory/src/usage.rs @@ -67,287 +67,368 @@ pub struct DailyBreakdown { pub calls: u64, } -/// Usage store backed by SQLite. -#[derive(Clone)] +/// Internal backend selector for the usage store. +enum UsageBackend { + Sqlite(Arc>), + Mongo(crate::mongo::usage::MongoUsageStore), +} + +/// Sync block-on helper for MongoDB async calls. +fn block_on(f: F) -> F::Output { + tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(f)) +} + +/// Usage store with dual SQLite/MongoDB backend support. pub struct UsageStore { - conn: Arc>, + backend: UsageBackend, +} + +// Manual Clone since we can't derive across the enum easily. +impl Clone for UsageStore { + fn clone(&self) -> Self { + match &self.backend { + UsageBackend::Sqlite(conn) => Self { + backend: UsageBackend::Sqlite(Arc::clone(conn)), + }, + UsageBackend::Mongo(store) => Self { + backend: UsageBackend::Mongo(store.clone()), + }, + } + } } impl UsageStore { - /// Create a new usage store wrapping the given connection. + /// Create a new usage store wrapping an SQLite connection. pub fn new(conn: Arc>) -> Self { - Self { conn } + Self { + backend: UsageBackend::Sqlite(conn), + } + } + + /// Create a new usage store wrapping a MongoDB usage store. + pub fn from_mongo(store: crate::mongo::usage::MongoUsageStore) -> Self { + Self { + backend: UsageBackend::Mongo(store), + } } /// Record a usage event. pub fn record(&self, record: &UsageRecord) -> OpenFangResult<()> { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; - let id = uuid::Uuid::new_v4().to_string(); - let now = Utc::now().to_rfc3339(); - conn.execute( - "INSERT INTO usage_events (id, agent_id, timestamp, model, input_tokens, output_tokens, cost_usd, tool_calls) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - rusqlite::params![ - id, - record.agent_id.0.to_string(), - now, - record.model, - record.input_tokens as i64, - record.output_tokens as i64, - record.cost_usd, - record.tool_calls as i64, - ], - ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - Ok(()) + match &self.backend { + UsageBackend::Sqlite(conn) => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Internal(e.to_string()))?; + let id = uuid::Uuid::new_v4().to_string(); + let now = Utc::now().to_rfc3339(); + conn.execute( + "INSERT INTO usage_events (id, agent_id, timestamp, model, input_tokens, output_tokens, cost_usd, tool_calls) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + rusqlite::params![ + id, + record.agent_id.0.to_string(), + now, + record.model, + record.input_tokens as i64, + record.output_tokens as i64, + record.cost_usd, + record.tool_calls as i64, + ], + ) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(()) + } + UsageBackend::Mongo(store) => block_on(store.record(record)), + } } /// Query total cost in the last hour for an agent. pub fn query_hourly(&self, agent_id: AgentId) -> OpenFangResult { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; - let cost: f64 = conn - .query_row( - "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events - WHERE agent_id = ?1 AND timestamp > datetime('now', '-1 hour')", - rusqlite::params![agent_id.0.to_string()], - |row| row.get(0), - ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - Ok(cost) + match &self.backend { + UsageBackend::Sqlite(conn) => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Internal(e.to_string()))?; + let cost: f64 = conn + .query_row( + "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events + WHERE agent_id = ?1 AND timestamp > datetime('now', '-1 hour')", + rusqlite::params![agent_id.0.to_string()], + |row| row.get(0), + ) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(cost) + } + UsageBackend::Mongo(store) => block_on(store.query_hourly(agent_id)), + } } /// Query total cost today for an agent. pub fn query_daily(&self, agent_id: AgentId) -> OpenFangResult { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; - let cost: f64 = conn - .query_row( - "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events - WHERE agent_id = ?1 AND timestamp > datetime('now', 'start of day')", - rusqlite::params![agent_id.0.to_string()], - |row| row.get(0), - ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - Ok(cost) + match &self.backend { + UsageBackend::Sqlite(conn) => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Internal(e.to_string()))?; + let cost: f64 = conn + .query_row( + "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events + WHERE agent_id = ?1 AND timestamp > datetime('now', 'start of day')", + rusqlite::params![agent_id.0.to_string()], + |row| row.get(0), + ) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(cost) + } + UsageBackend::Mongo(store) => block_on(store.query_daily(agent_id)), + } } /// Query total cost in the current calendar month for an agent. pub fn query_monthly(&self, agent_id: AgentId) -> OpenFangResult { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; - let cost: f64 = conn - .query_row( - "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events - WHERE agent_id = ?1 AND timestamp > datetime('now', 'start of month')", - rusqlite::params![agent_id.0.to_string()], - |row| row.get(0), - ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - Ok(cost) + match &self.backend { + UsageBackend::Sqlite(conn) => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Internal(e.to_string()))?; + let cost: f64 = conn + .query_row( + "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events + WHERE agent_id = ?1 AND timestamp > datetime('now', 'start of month')", + rusqlite::params![agent_id.0.to_string()], + |row| row.get(0), + ) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(cost) + } + UsageBackend::Mongo(store) => block_on(store.query_monthly(agent_id)), + } } /// Query total cost across all agents for the current hour. pub fn query_global_hourly(&self) -> OpenFangResult { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; - let cost: f64 = conn - .query_row( - "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events - WHERE timestamp > datetime('now', '-1 hour')", - [], - |row| row.get(0), - ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - Ok(cost) + match &self.backend { + UsageBackend::Sqlite(conn) => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Internal(e.to_string()))?; + let cost: f64 = conn + .query_row( + "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events + WHERE timestamp > datetime('now', '-1 hour')", + [], + |row| row.get(0), + ) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(cost) + } + UsageBackend::Mongo(store) => block_on(store.query_global_hourly()), + } } /// Query total cost across all agents for the current calendar month. pub fn query_global_monthly(&self) -> OpenFangResult { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; - let cost: f64 = conn - .query_row( - "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events - WHERE timestamp > datetime('now', 'start of month')", - [], - |row| row.get(0), - ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - Ok(cost) + match &self.backend { + UsageBackend::Sqlite(conn) => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Internal(e.to_string()))?; + let cost: f64 = conn + .query_row( + "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events + WHERE timestamp > datetime('now', 'start of month')", + [], + |row| row.get(0), + ) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(cost) + } + UsageBackend::Mongo(store) => block_on(store.query_global_monthly()), + } } /// Query usage summary, optionally filtered by agent. pub fn query_summary(&self, agent_id: Option) -> OpenFangResult { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; - - let (sql, params): (&str, Vec>) = match agent_id { - Some(aid) => ( - "SELECT COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0), - COALESCE(SUM(cost_usd), 0.0), COUNT(*), COALESCE(SUM(tool_calls), 0) - FROM usage_events WHERE agent_id = ?1", - vec![Box::new(aid.0.to_string())], - ), - None => ( - "SELECT COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0), - COALESCE(SUM(cost_usd), 0.0), COUNT(*), COALESCE(SUM(tool_calls), 0) - FROM usage_events", - vec![], - ), - }; - - let params_refs: Vec<&dyn rusqlite::types::ToSql> = - params.iter().map(|p| p.as_ref()).collect(); - - let summary = conn - .query_row(sql, params_refs.as_slice(), |row| { - Ok(UsageSummary { - total_input_tokens: row.get::<_, i64>(0)? as u64, - total_output_tokens: row.get::<_, i64>(1)? as u64, - total_cost_usd: row.get(2)?, - call_count: row.get::<_, i64>(3)? as u64, - total_tool_calls: row.get::<_, i64>(4)? as u64, - }) - }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - - Ok(summary) + match &self.backend { + UsageBackend::Sqlite(conn) => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Internal(e.to_string()))?; + + let (sql, params): (&str, Vec>) = match agent_id { + Some(aid) => ( + "SELECT COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0), + COALESCE(SUM(cost_usd), 0.0), COUNT(*), COALESCE(SUM(tool_calls), 0) + FROM usage_events WHERE agent_id = ?1", + vec![Box::new(aid.0.to_string())], + ), + None => ( + "SELECT COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0), + COALESCE(SUM(cost_usd), 0.0), COUNT(*), COALESCE(SUM(tool_calls), 0) + FROM usage_events", + vec![], + ), + }; + + let params_refs: Vec<&dyn rusqlite::types::ToSql> = + params.iter().map(|p| p.as_ref()).collect(); + + let summary = conn + .query_row(sql, params_refs.as_slice(), |row| { + Ok(UsageSummary { + total_input_tokens: row.get::<_, i64>(0)? as u64, + total_output_tokens: row.get::<_, i64>(1)? as u64, + total_cost_usd: row.get(2)?, + call_count: row.get::<_, i64>(3)? as u64, + total_tool_calls: row.get::<_, i64>(4)? as u64, + }) + }) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + Ok(summary) + } + UsageBackend::Mongo(store) => block_on(store.query_summary(agent_id)), + } } /// Query usage grouped by model. pub fn query_by_model(&self) -> OpenFangResult> { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; - - let mut stmt = conn - .prepare( - "SELECT model, COALESCE(SUM(cost_usd), 0.0), COALESCE(SUM(input_tokens), 0), - COALESCE(SUM(output_tokens), 0), COUNT(*) - FROM usage_events GROUP BY model ORDER BY SUM(cost_usd) DESC", - ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - - let rows = stmt - .query_map([], |row| { - Ok(ModelUsage { - model: row.get(0)?, - total_cost_usd: row.get(1)?, - total_input_tokens: row.get::<_, i64>(2)? as u64, - total_output_tokens: row.get::<_, i64>(3)? as u64, - call_count: row.get::<_, i64>(4)? as u64, - }) - }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - - let mut results = Vec::new(); - for row in rows { - results.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + match &self.backend { + UsageBackend::Sqlite(conn) => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Internal(e.to_string()))?; + + let mut stmt = conn + .prepare( + "SELECT model, COALESCE(SUM(cost_usd), 0.0), COALESCE(SUM(input_tokens), 0), + COALESCE(SUM(output_tokens), 0), COUNT(*) + FROM usage_events GROUP BY model ORDER BY SUM(cost_usd) DESC", + ) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let rows = stmt + .query_map([], |row| { + Ok(ModelUsage { + model: row.get(0)?, + total_cost_usd: row.get(1)?, + total_input_tokens: row.get::<_, i64>(2)? as u64, + total_output_tokens: row.get::<_, i64>(3)? as u64, + call_count: row.get::<_, i64>(4)? as u64, + }) + }) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut results = Vec::new(); + for row in rows { + results.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + } + Ok(results) + } + UsageBackend::Mongo(store) => block_on(store.query_by_model()), } - Ok(results) } /// Query daily usage breakdown for the last N days. pub fn query_daily_breakdown(&self, days: u32) -> OpenFangResult> { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; - - let mut stmt = conn - .prepare(&format!( - "SELECT date(timestamp) as day, - COALESCE(SUM(cost_usd), 0.0), - COALESCE(SUM(input_tokens) + SUM(output_tokens), 0), - COUNT(*) - FROM usage_events - WHERE timestamp > datetime('now', '-{days} days') - GROUP BY day - ORDER BY day ASC" - )) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - - let rows = stmt - .query_map([], |row| { - Ok(DailyBreakdown { - date: row.get(0)?, - cost_usd: row.get(1)?, - tokens: row.get::<_, i64>(2)? as u64, - calls: row.get::<_, i64>(3)? as u64, - }) - }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - - let mut results = Vec::new(); - for row in rows { - results.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + match &self.backend { + UsageBackend::Sqlite(conn) => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Internal(e.to_string()))?; + + let mut stmt = conn + .prepare(&format!( + "SELECT date(timestamp) as day, + COALESCE(SUM(cost_usd), 0.0), + COALESCE(SUM(input_tokens) + SUM(output_tokens), 0), + COUNT(*) + FROM usage_events + WHERE timestamp > datetime('now', '-{days} days') + GROUP BY day + ORDER BY day ASC" + )) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let rows = stmt + .query_map([], |row| { + Ok(DailyBreakdown { + date: row.get(0)?, + cost_usd: row.get(1)?, + tokens: row.get::<_, i64>(2)? as u64, + calls: row.get::<_, i64>(3)? as u64, + }) + }) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + + let mut results = Vec::new(); + for row in rows { + results.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + } + Ok(results) + } + UsageBackend::Mongo(store) => block_on(store.query_daily_breakdown(days)), } - Ok(results) } /// Query the timestamp of the earliest usage event. pub fn query_first_event_date(&self) -> OpenFangResult> { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; - let result: Option = conn - .query_row("SELECT MIN(timestamp) FROM usage_events", [], |row| { - row.get(0) - }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - Ok(result) + match &self.backend { + UsageBackend::Sqlite(conn) => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Internal(e.to_string()))?; + let result: Option = conn + .query_row("SELECT MIN(timestamp) FROM usage_events", [], |row| { + row.get(0) + }) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(result) + } + UsageBackend::Mongo(store) => block_on(store.query_first_event_date()), + } } /// Query today's total cost across all agents. pub fn query_today_cost(&self) -> OpenFangResult { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; - let cost: f64 = conn - .query_row( - "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events - WHERE timestamp > datetime('now', 'start of day')", - [], - |row| row.get(0), - ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - Ok(cost) + match &self.backend { + UsageBackend::Sqlite(conn) => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Internal(e.to_string()))?; + let cost: f64 = conn + .query_row( + "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events + WHERE timestamp > datetime('now', 'start of day')", + [], + |row| row.get(0), + ) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(cost) + } + UsageBackend::Mongo(store) => block_on(store.query_today_cost()), + } } /// Delete usage events older than the given number of days. pub fn cleanup_old(&self, days: u32) -> OpenFangResult { - let conn = self - .conn - .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; - let deleted = conn - .execute( - &format!( - "DELETE FROM usage_events WHERE timestamp < datetime('now', '-{days} days')" - ), - [], - ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - Ok(deleted) + match &self.backend { + UsageBackend::Sqlite(conn) => { + let conn = conn + .lock() + .map_err(|e| OpenFangError::Internal(e.to_string()))?; + let deleted = conn + .execute( + &format!( + "DELETE FROM usage_events WHERE timestamp < datetime('now', '-{days} days')" + ), + [], + ) + .map_err(|e| OpenFangError::Memory(e.to_string()))?; + Ok(deleted) + } + UsageBackend::Mongo(store) => block_on(store.cleanup_old(days)), + } } } diff --git a/crates/openfang-runtime/Cargo.toml b/crates/openfang-runtime/Cargo.toml index 065cde421..569bd4e8b 100644 --- a/crates/openfang-runtime/Cargo.toml +++ b/crates/openfang-runtime/Cargo.toml @@ -30,6 +30,8 @@ zeroize = { workspace = true } dashmap = { workspace = true } regex-lite = { workspace = true } rusqlite = { workspace = true } +mongodb = { workspace = true } +bson = { workspace = true } tokio-tungstenite = "0.24" shlex = "1" diff --git a/crates/openfang-runtime/src/audit.rs b/crates/openfang-runtime/src/audit.rs index 4aef914ac..f0c40a342 100644 --- a/crates/openfang-runtime/src/audit.rs +++ b/crates/openfang-runtime/src/audit.rs @@ -4,8 +4,8 @@ //! contains the SHA-256 hash of its own contents concatenated with the hash of //! the previous entry, forming a tamper-evident chain (similar to a blockchain). //! -//! When a database connection is provided (`with_db`), entries are persisted to -//! the `audit_entries` table (schema V8) so the trail survives daemon restarts. +//! When a database connection is provided (`with_db` or `with_mongo_db`), entries +//! are persisted so the trail survives daemon restarts. use chrono::Utc; use rusqlite::Connection; @@ -78,15 +78,40 @@ fn compute_entry_hash( hex::encode(hasher.finalize()) } +/// Parse an action string into an AuditAction enum. +fn parse_action(s: &str) -> AuditAction { + match s { + "ToolInvoke" => AuditAction::ToolInvoke, + "CapabilityCheck" => AuditAction::CapabilityCheck, + "AgentSpawn" => AuditAction::AgentSpawn, + "AgentKill" => AuditAction::AgentKill, + "AgentMessage" => AuditAction::AgentMessage, + "MemoryAccess" => AuditAction::MemoryAccess, + "FileAccess" => AuditAction::FileAccess, + "NetworkAccess" => AuditAction::NetworkAccess, + "ShellExec" => AuditAction::ShellExec, + "AuthAttempt" => AuditAction::AuthAttempt, + "WireConnect" => AuditAction::WireConnect, + "ConfigChange" => AuditAction::ConfigChange, + _ => AuditAction::ToolInvoke, // fallback + } +} + +/// Backend selector for audit log persistence. +enum AuditDb { + Sqlite(Arc>), + Mongo(mongodb::Collection), +} + /// An append-only, tamper-evident audit log using a Merkle hash chain. /// /// Thread-safe — all access is serialised through internal mutexes. -/// Optionally backed by SQLite for persistence across daemon restarts. +/// Optionally backed by SQLite or MongoDB for persistence across daemon restarts. pub struct AuditLog { entries: Mutex>, tip: Mutex, - /// Optional database connection for persistent storage. - db: Option>>, + /// Optional database backend for persistent storage. + db: Option, } impl AuditLog { @@ -101,7 +126,7 @@ impl AuditLog { } } - /// Creates an audit log backed by a database connection. + /// Creates an audit log backed by a SQLite database connection. /// /// On construction, loads all existing entries from the `audit_entries` /// table and verifies the Merkle chain integrity. New entries are written @@ -118,26 +143,11 @@ impl AuditLog { if let Ok(mut stmt) = result { let rows = stmt.query_map([], |row| { let action_str: String = row.get(3)?; - let action = match action_str.as_str() { - "ToolInvoke" => AuditAction::ToolInvoke, - "CapabilityCheck" => AuditAction::CapabilityCheck, - "AgentSpawn" => AuditAction::AgentSpawn, - "AgentKill" => AuditAction::AgentKill, - "AgentMessage" => AuditAction::AgentMessage, - "MemoryAccess" => AuditAction::MemoryAccess, - "FileAccess" => AuditAction::FileAccess, - "NetworkAccess" => AuditAction::NetworkAccess, - "ShellExec" => AuditAction::ShellExec, - "AuthAttempt" => AuditAction::AuthAttempt, - "WireConnect" => AuditAction::WireConnect, - "ConfigChange" => AuditAction::ConfigChange, - _ => AuditAction::ToolInvoke, // fallback - }; Ok(AuditEntry { seq: row.get(0)?, timestamp: row.get(1)?, agent_id: row.get(2)?, - action, + action: parse_action(&action_str), detail: row.get(4)?, outcome: row.get(5)?, prev_hash: row.get(6)?, @@ -157,7 +167,7 @@ impl AuditLog { let log = Self { entries: Mutex::new(entries), tip: Mutex::new(tip), - db: Some(conn), + db: Some(AuditDb::Sqlite(conn)), }; // Verify chain integrity on load @@ -172,11 +182,84 @@ impl AuditLog { log } + /// Creates an audit log backed by a MongoDB database. + /// + /// On construction, loads all existing entries from the `audit_entries` + /// collection and verifies the Merkle chain integrity. + pub fn with_mongo_db(db: mongodb::Database) -> Self { + let collection: mongodb::Collection = db.collection("audit_entries"); + let mut entries = Vec::new(); + let mut tip = "0".repeat(64); + + // Load existing entries from MongoDB (boot-time only, use block_in_place) + let load_result: Result, String> = + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + use bson::doc; + use futures::TryStreamExt; + + let opts = mongodb::options::FindOptions::builder() + .sort(doc! { "seq": 1 }) + .build(); + let mut cursor = collection + .find(doc! {}) + .with_options(opts) + .await + .map_err(|e| e.to_string())?; + + let mut loaded = Vec::new(); + while let Some(d) = cursor.try_next().await.map_err(|e| e.to_string())? { + let entry = AuditEntry { + seq: d.get_i64("seq").unwrap_or(0) as u64, + timestamp: d.get_str("timestamp").unwrap_or("").to_string(), + agent_id: d.get_str("agent_id").unwrap_or("").to_string(), + action: parse_action(d.get_str("action").unwrap_or("")), + detail: d.get_str("detail").unwrap_or("").to_string(), + outcome: d.get_str("outcome").unwrap_or("").to_string(), + prev_hash: d.get_str("prev_hash").unwrap_or("").to_string(), + hash: d.get_str("hash").unwrap_or("").to_string(), + }; + loaded.push(entry); + } + Ok(loaded) + }) + }); + + match load_result { + Ok(loaded) => { + for entry in loaded { + tip = entry.hash.clone(); + entries.push(entry); + } + } + Err(e) => { + tracing::error!("Failed to load audit entries from MongoDB: {e}"); + } + } + + let count = entries.len(); + let log = Self { + entries: Mutex::new(entries), + tip: Mutex::new(tip), + db: Some(AuditDb::Mongo(collection)), + }; + + if count > 0 { + if let Err(e) = log.verify_integrity() { + tracing::error!("Audit trail integrity check FAILED on boot: {e}"); + } else { + tracing::info!("Audit trail loaded: {count} entries, chain integrity OK"); + } + } + + log + } + /// Records a new auditable event and returns the SHA-256 hash of the entry. /// /// The entry is atomically appended to the chain with the current tip as /// its `prev_hash`, and the tip is advanced to the new hash. - /// If a database connection is available, the entry is also persisted. + /// If a database backend is available, the entry is also persisted. pub fn record( &self, agent_id: impl Into, @@ -211,22 +294,44 @@ impl AuditLog { }; // Persist to database if available - if let Some(ref db) = self.db { - if let Ok(conn) = db.lock() { - let _ = conn.execute( - "INSERT INTO audit_entries (seq, timestamp, agent_id, action, detail, outcome, prev_hash, hash) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - rusqlite::params![ - entry.seq as i64, - &entry.timestamp, - &entry.agent_id, - entry.action.to_string(), - &entry.detail, - &entry.outcome, - &entry.prev_hash, - &entry.hash, - ], - ); + match &self.db { + Some(AuditDb::Sqlite(db)) => { + if let Ok(conn) = db.lock() { + let _ = conn.execute( + "INSERT INTO audit_entries (seq, timestamp, agent_id, action, detail, outcome, prev_hash, hash) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + rusqlite::params![ + entry.seq as i64, + &entry.timestamp, + &entry.agent_id, + entry.action.to_string(), + &entry.detail, + &entry.outcome, + &entry.prev_hash, + &entry.hash, + ], + ); + } + } + Some(AuditDb::Mongo(collection)) => { + let doc = bson::doc! { + "seq": entry.seq as i64, + "timestamp": &entry.timestamp, + "agent_id": &entry.agent_id, + "action": entry.action.to_string(), + "detail": &entry.detail, + "outcome": &entry.outcome, + "prev_hash": &entry.prev_hash, + "hash": &entry.hash, + }; + let coll = collection.clone(); + // Fire-and-forget async insert + tokio::spawn(async move { + if let Err(e) = coll.insert_one(doc).await { + tracing::warn!("Failed to persist audit entry to MongoDB: {e}"); + } + }); } + None => {} } entries.push(entry); diff --git a/crates/openfang-runtime/src/kernel_handle.rs b/crates/openfang-runtime/src/kernel_handle.rs index 00f195599..550b9e2f8 100644 --- a/crates/openfang-runtime/src/kernel_handle.rs +++ b/crates/openfang-runtime/src/kernel_handle.rs @@ -22,8 +22,8 @@ pub struct AgentInfo { /// Handle to kernel operations, passed into the agent loop so agents /// can interact with each other via tools. -#[allow(clippy::too_many_arguments)] #[async_trait] +#[allow(clippy::too_many_arguments)] pub trait KernelHandle: Send + Sync { /// Spawn a new agent from a TOML manifest string. /// `parent_id` is the UUID string of the spawning agent (for lineage tracking). diff --git a/crates/openfang-types/src/config.rs b/crates/openfang-types/src/config.rs index 71748eb02..7aa67d989 100644 --- a/crates/openfang-types/src/config.rs +++ b/crates/openfang-types/src/config.rs @@ -1471,8 +1471,17 @@ impl Default for DefaultModelConfig { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] pub struct MemoryConfig { - /// Path to SQLite database file. + /// Storage backend: "sqlite" (default) or "mongodb". + #[serde(default = "default_memory_backend")] + pub backend: String, + /// Path to SQLite database file (used when backend = "sqlite"). pub sqlite_path: Option, + /// MongoDB connection string (used when backend = "mongodb"). + #[serde(default = "default_mongo_url")] + pub mongo_url: String, + /// MongoDB database name (used when backend = "mongodb"). + #[serde(default = "default_mongo_db")] + pub mongo_db_name: String, /// Embedding model for semantic search. pub embedding_model: String, /// Maximum memories before consolidation is triggered. @@ -1490,6 +1499,18 @@ pub struct MemoryConfig { pub consolidation_interval_hours: u64, } +fn default_memory_backend() -> String { + "sqlite".to_string() +} + +fn default_mongo_url() -> String { + "mongodb://localhost:27017".to_string() +} + +fn default_mongo_db() -> String { + "openfang".to_string() +} + fn default_consolidation_interval() -> u64 { 24 } @@ -1497,7 +1518,10 @@ fn default_consolidation_interval() -> u64 { impl Default for MemoryConfig { fn default() -> Self { Self { + backend: default_memory_backend(), sqlite_path: None, + mongo_url: default_mongo_url(), + mongo_db_name: default_mongo_db(), embedding_model: "all-MiniLM-L6-v2".to_string(), consolidation_threshold: 10_000, decay_rate: 0.1,