diff --git a/.cargo/config.toml b/.cargo/config.toml index 09b8772..cb95ce4 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -37,7 +37,6 @@ debug = true debug-assertions = true overflow-checks = true lto = false -panic = "unwind" incremental = true [profile.bench] @@ -48,12 +47,8 @@ debug-assertions = false overflow-checks = false lto = "thin" codegen-units = 1 -panic = "abort" incremental = false -# Resolver version -resolver = "2" - [term] # Terminal colors color = "auto" diff --git a/Cargo.lock b/Cargo.lock index a51be54..ff7473d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - [[package]] name = "addr" version = "0.15.6" @@ -23,27 +17,19 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - [[package]] name = "affinitypool" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dde2a385b82232b559baeec740c37809051c596f9b56e7da0d0da2c8e8f54f6" +checksum = "7a58b64a64aecad4ba7f2ccf0f79115f5d2d184b1e55307f78c20be07adc6633" dependencies = [ - "async-channel", + "crossbeam", + "libc", "num_cpus", - "thiserror 1.0.69", + "parking_lot", + "thiserror 2.0.18", "tokio", + "winapi", ] [[package]] @@ -142,27 +128,12 @@ dependencies = [ "libc", ] -[[package]] -name = "any_ascii" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90c6333e01ba7235575b6ab53e5af10f1c327927fd97c36462917e289557ea64" - [[package]] name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" -[[package]] -name = "approx" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" -dependencies = [ - "num-traits", -] - [[package]] name = "approx" version = "0.5.1" @@ -225,6 +196,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "arrayvec" version = "0.7.6" @@ -237,19 +214,40 @@ version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e833808ff2d94ed40d9379848a950d995043c7fb3e81a30b383f4c6033821cc" dependencies = [ - "arrow-arith", - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-csv", - "arrow-data", - "arrow-ipc", - "arrow-json", - "arrow-ord", - "arrow-row", - "arrow-schema", - "arrow-select", - "arrow-string", + "arrow-arith 56.2.0", + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-cast 56.2.0", + "arrow-csv 56.2.0", + "arrow-data 56.2.0", + "arrow-ipc 56.2.0", + "arrow-json 56.2.0", + "arrow-ord 56.2.0", + "arrow-row 56.2.0", + "arrow-schema 56.2.0", + "arrow-select 56.2.0", + "arrow-string 56.2.0", +] + +[[package]] +name = "arrow" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4754a624e5ae42081f464514be454b39711daae0458906dacde5f4c632f33a8" +dependencies = [ + "arrow-arith 57.3.0", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-cast 57.3.0", + "arrow-csv 57.3.0", + "arrow-data 57.3.0", + "arrow-ipc 57.3.0", + "arrow-json 57.3.0", + "arrow-ord 57.3.0", + "arrow-row 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", + "arrow-string 57.3.0", ] [[package]] @@ -258,14 +256,28 @@ version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad08897b81588f60ba983e3ca39bda2b179bdd84dced378e7df81a5313802ef8" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", "chrono", "num", ] +[[package]] +name = "arrow-arith" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7b3141e0ec5145a22d8694ea8b6d6f69305971c4fa1c1a13ef0195aef2d678b" +dependencies = [ + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", + "chrono", + "num-traits", +] + [[package]] name = "arrow-array" version = "56.2.0" @@ -273,14 +285,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8548ca7c070d8db9ce7aa43f37393e4bfcf3f2d3681df278490772fd1673d08d" dependencies = [ "ahash 0.8.12", - "arrow-buffer", - "arrow-data", - "arrow-schema", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "chrono", + "half", + "hashbrown 0.16.1", + "num", +] + +[[package]] +name = "arrow-array" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8955af33b25f3b175ee10af580577280b4bd01f7e823d94c7cdef7cf8c9aef" +dependencies = [ + "ahash 0.8.12", + "arrow-buffer 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", "chrono", "chrono-tz", "half", "hashbrown 0.16.1", - "num", + "num-complex", + "num-integer", + "num-traits", ] [[package]] @@ -294,24 +324,57 @@ dependencies = [ "num", ] +[[package]] +name = "arrow-buffer" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c697ddca96183182f35b3a18e50b9110b11e916d7b7799cbfd4d34662f2c56c2" +dependencies = [ + "bytes", + "half", + "num-bigint", + "num-traits", +] + [[package]] name = "arrow-cast" version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "919418a0681298d3a77d1a315f625916cb5678ad0d74b9c60108eb15fd083023" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "arrow-select 56.2.0", + "atoi", + "base64 0.22.1", + "chrono", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-cast" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "646bbb821e86fd57189c10b4fcdaa941deaf4181924917b0daa92735baa6ada5" +dependencies = [ + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-data 57.3.0", + "arrow-ord 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", "atoi", "base64 0.22.1", "chrono", "comfy-table", "half", "lexical-core", - "num", + "num-traits", "ryu", ] @@ -321,9 +384,24 @@ version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa9bf02705b5cf762b6f764c65f04ae9082c7cfc4e96e0c33548ee3f67012eb" dependencies = [ - "arrow-array", - "arrow-cast", - "arrow-schema", + "arrow-array 56.2.0", + "arrow-cast 56.2.0", + "arrow-schema 56.2.0", + "chrono", + "csv", + "csv-core", + "regex", +] + +[[package]] +name = "arrow-csv" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da746f4180004e3ce7b83c977daf6394d768332349d3d913998b10a120b790a" +dependencies = [ + "arrow-array 57.3.0", + "arrow-cast 57.3.0", + "arrow-schema 57.3.0", "chrono", "csv", "csv-core", @@ -336,25 +414,52 @@ version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5c64fff1d142f833d78897a772f2e5b55b36cb3e6320376f0961ab0db7bd6d0" dependencies = [ - "arrow-buffer", - "arrow-schema", + "arrow-buffer 56.2.0", + "arrow-schema 56.2.0", "half", "num", ] +[[package]] +name = "arrow-data" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fdd994a9d28e6365aa78e15da3f3950c0fdcea6b963a12fa1c391afb637b304" +dependencies = [ + "arrow-buffer 57.3.0", + "arrow-schema 57.3.0", + "half", + "num-integer", + "num-traits", +] + [[package]] name = "arrow-ipc" version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d3594dcddccc7f20fd069bc8e9828ce37220372680ff638c5e00dea427d88f5" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "arrow-select 56.2.0", "flatbuffers", - "lz4_flex", +] + +[[package]] +name = "arrow-ipc" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf7df950701ab528bf7c0cf7eeadc0445d03ef5d6ffc151eaae6b38a58feff1" +dependencies = [ + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", + "flatbuffers", + "lz4_flex 0.12.0", "zstd", ] @@ -364,11 +469,11 @@ version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88cf36502b64a127dc659e3b305f1d993a544eab0d48cce704424e62074dc04b" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-data", - "arrow-schema", + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-cast 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", "chrono", "half", "indexmap 2.13.0", @@ -380,17 +485,54 @@ dependencies = [ "simdutf8", ] +[[package]] +name = "arrow-json" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ff8357658bedc49792b13e2e862b80df908171275f8e6e075c460da5ee4bf86" +dependencies = [ + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-cast 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", + "chrono", + "half", + "indexmap 2.13.0", + "itoa", + "lexical-core", + "memchr", + "num-traits", + "ryu", + "serde_core", + "serde_json", + "simdutf8", +] + [[package]] name = "arrow-ord" version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c8f82583eb4f8d84d4ee55fd1cb306720cddead7596edce95b50ee418edf66f" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "arrow-select 56.2.0", +] + +[[package]] +name = "arrow-ord" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d8f1870e03d4cbed632959498bcc84083b5a24bded52905ae1695bd29da45b" +dependencies = [ + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", ] [[package]] @@ -399,10 +541,23 @@ version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d07ba24522229d9085031df6b94605e0f4b26e099fb7cdeec37abd941a73753" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "half", +] + +[[package]] +name = "arrow-row" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18228633bad92bff92a95746bbeb16e5fc318e8382b75619dec26db79e4de4c0" +dependencies = [ + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", "half", ] @@ -411,9 +566,15 @@ name = "arrow-schema" version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3aa9e59c611ebc291c28582077ef25c97f1975383f1479b12f3b9ffee2ffabe" + +[[package]] +name = "arrow-schema" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68" dependencies = [ "bitflags 2.10.0", - "serde", + "serde_core", "serde_json", ] @@ -424,30 +585,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c41dbbd1e97bfcaee4fcb30e29105fb2c75e4d82ae4de70b792a5d3f66b2e7a" dependencies = [ "ahash 0.8.12", - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", "num", ] +[[package]] +name = "arrow-select" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bf3e3efbd1278f770d67e5dc410257300b161b93baedb3aae836144edcaf4b" +dependencies = [ + "ahash 0.8.12", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", + "num-traits", +] + [[package]] name = "arrow-string" version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53f5183c150fbc619eede22b861ea7c0eebed8eaac0333eaa7f6da5205fd504d" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "arrow-select 56.2.0", "memchr", "num", "regex", "regex-syntax", ] +[[package]] +name = "arrow-string" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e968097061b3c0e9fe3079cf2e703e487890700546b5b0647f60fca1b5a8d8" +dependencies = [ + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", + "memchr", + "num-traits", + "regex", + "regex-syntax", +] + [[package]] name = "as-slice" version = "0.1.5" @@ -471,9 +663,9 @@ dependencies = [ [[package]] name = "ascii-canvas" -version = "3.0.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" dependencies = [ "term", ] @@ -496,118 +688,11 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06575e6a9673580f52661c92107baabffbf41e2141373441cbcdc47cb733003c" dependencies = [ - "bzip2 0.5.2", "flate2", "futures-core", "memchr", "pin-project-lite", "tokio", - "xz2", - "zstd", - "zstd-safe", -] - -[[package]] -name = "async-executor" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-graphql" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1057a9f7ccf2404d94571dec3451ade1cb524790df6f1ada0d19c2a49f6b0f40" -dependencies = [ - "async-graphql-derive", - "async-graphql-parser", - "async-graphql-value", - "async-io", - "async-trait", - "asynk-strim", - "base64 0.22.1", - "bytes", - "fnv", - "futures-util", - "http 1.4.0", - "indexmap 2.13.0", - "mime", - "multer", - "num-traits", - "pin-project-lite", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "static_assertions_next", - "thiserror 2.0.18", -] - -[[package]] -name = "async-graphql-derive" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6cbeadc8515e66450fba0985ce722192e28443697799988265d86304d7cc68" -dependencies = [ - "Inflector", - "async-graphql-parser", - "darling 0.23.0", - "proc-macro-crate", - "proc-macro2", - "quote", - "strum 0.27.2", - "syn 2.0.114", - "thiserror 2.0.18", -] - -[[package]] -name = "async-graphql-parser" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64ef70f77a1c689111e52076da1cd18f91834bcb847de0a9171f83624b07fbf" -dependencies = [ - "async-graphql-value", - "pest", - "serde", - "serde_json", -] - -[[package]] -name = "async-graphql-value" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e3ef112905abea9dea592fc868a6873b10ebd3f983e83308f995d6284e9ba41" -dependencies = [ - "bytes", - "indexmap 2.13.0", - "serde", - "serde_json", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 1.1.3", - "slab", - "windows-sys 0.61.2", ] [[package]] @@ -621,6 +706,43 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-nats" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5af9ebfb0a14481d3eaf6101e6391261e4f30d25b26a7635ade8a39482ded0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-util", + "memchr", + "nkeys", + "nuid", + "once_cell", + "pin-project", + "portable-atomic", + "rand 0.8.5", + "regex", + "ring", + "rustls-native-certs 0.7.3", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "serde", + "serde_json", + "serde_nanos", + "serde_repr", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-rustls", + "tokio-stream", + "tokio-util", + "tokio-websockets", + "tracing", + "tryhard", + "url", +] + [[package]] name = "async-recursion" version = "1.1.1" @@ -633,10 +755,26 @@ dependencies = [ ] [[package]] -name = "async-task" -version = "4.7.1" +name = "async-stream" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] [[package]] name = "async-trait" @@ -658,27 +796,6 @@ dependencies = [ "loom", ] -[[package]] -name = "async_io_stream" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" -dependencies = [ - "futures", - "pharos", - "rustc_version", -] - -[[package]] -name = "asynk-strim" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52697735bdaac441a29391a9e97102c74c6ef0f9b60a40cf109b1b404e29d2f6" -dependencies = [ - "futures-core", - "pin-project-lite", -] - [[package]] name = "atoi" version = "2.0.0" @@ -718,7 +835,7 @@ dependencies = [ "aligned", "anyhow", "arg_enum_proc_macro", - "arrayvec", + "arrayvec 0.7.6", "log", "num-rational", "num-traits", @@ -736,7 +853,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" dependencies = [ "anyhow", - "arrayvec", + "arrayvec 0.7.6", "log", "nom 8.0.0", "num-rational", @@ -749,49 +866,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" dependencies = [ - "arrayvec", -] - -[[package]] -name = "aws-config" -version = "1.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96571e6996817bf3d58f6b569e4b9fd2e9d2fcf9f7424eed07b2ce9bb87535e5" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sdk-sso", - "aws-sdk-ssooidc", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "hex", - "http 1.4.0", - "ring", - "time", - "tokio", - "tracing", - "url", - "zeroize", -] - -[[package]] -name = "aws-credential-types" -version = "1.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", + "arrayvec 0.7.6", ] [[package]] @@ -801,6 +876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" dependencies = [ "aws-lc-sys", + "untrusted 0.7.1", "zeroize", ] @@ -817,334 +893,62 @@ dependencies = [ ] [[package]] -name = "aws-runtime" -version = "1.5.18" +name = "axum" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "959dab27ce613e6c9658eb3621064d0e2027e5f2acb65bc526a43577facea557" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "tracing", - "uuid", -] - -[[package]] -name = "aws-sdk-dynamodb" -version = "1.103.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df2a8b03419775bfaf4f3ebbb65a9772e9e69eed4467a1b33f22226722340fb" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sso" -version = "1.92.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d63bd2bdeeb49aa3f9b00c15e18583503b778b2e792fc06284d54e7d5b6566" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-ssooidc" -version = "1.94.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532d93574bf731f311bafb761366f9ece345a0416dbcc273d81d6d1a1205239b" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.96.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357e9a029c7524db6a0099cd77fbd5da165540339e7296cca603531bc783b56c" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e523e1c4e8e7e8ff219d732988e22bfeae8a1cafdbe6d9eca1546fa080be7c" -dependencies = [ - "aws-credential-types", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", + "axum-core", "bytes", "form_urlencoded", - "hex", - "hmac", - "http 0.2.12", - "http 1.4.0", - "percent-encoding", - "sha2", - "time", - "tracing", -] - -[[package]] -name = "aws-smithy-async" -version = "1.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee19095c7c4dda59f1697d028ce704c24b2d33c6718790c7f1d5a3015b4107c" -dependencies = [ "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-http" -version = "0.62.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" -dependencies = [ - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "futures-util", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http-client" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e62db736db19c488966c8d787f52e6270be565727236fd5579eaa301e7bc4a" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "h2 0.3.27", - "h2 0.4.13", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper 1.8.1", - "hyper-rustls 0.24.2", - "hyper-rustls 0.27.7", + "http", + "http-body", + "http-body-util", + "hyper", "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", "pin-project-lite", - "rustls 0.21.12", - "rustls 0.23.36", - "rustls-native-certs", - "rustls-pki-types", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", "tokio", - "tokio-rustls 0.26.4", "tower", + "tower-layer", + "tower-service", "tracing", ] [[package]] -name = "aws-smithy-json" -version = "0.61.9" +name = "axum-core" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ - "aws-smithy-types", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", ] [[package]] -name = "aws-smithy-observability" +name = "base16ct" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1fcbefc7ece1d70dcce29e490f269695dfca2d2bacdeaf9e5c3f799e4e6a42" -dependencies = [ - "aws-smithy-runtime-api", -] - -[[package]] -name = "aws-smithy-query" -version = "0.60.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5d689cf437eae90460e944a58b5668530d433b4ff85789e69d2f2a556e057d" -dependencies = [ - "aws-smithy-types", - "urlencoding", -] - -[[package]] -name = "aws-smithy-runtime" -version = "1.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb5b6167fcdf47399024e81ac08e795180c576a20e4d4ce67949f9a88ae37dc1" -dependencies = [ - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-http-client", - "aws-smithy-observability", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "pin-project-lite", - "pin-utils", - "tokio", - "tracing", -] - -[[package]] -name = "aws-smithy-runtime-api" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efce7aaaf59ad53c5412f14fc19b2d5c6ab2c3ec688d272fd31f76ec12f44fb0" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes", - "http 0.2.12", - "http 1.4.0", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-types" -version = "1.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f172bcb02424eb94425db8aed1b6d583b5104d4d5ddddf22402c661a320048" -dependencies = [ - "base64-simd", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time", - "tokio", - "tokio-util", -] - -[[package]] -name = "aws-smithy-xml" -version = "0.60.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "aws-types" -version = "1.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164" -dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "rustc_version", - "tracing", -] - -[[package]] -name = "backon" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" -dependencies = [ - "fastrand", - "gloo-timers", - "tokio", -] +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" @@ -1152,28 +956,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - [[package]] name = "base64ct" version = "1.8.3" @@ -1182,13 +970,13 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bcrypt" -version = "0.15.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" +checksum = "9a0f5948f30df5f43ac29d310b7476793be97c50787e6ef4a63d960a0d0be827" dependencies = [ "base64 0.22.1", "blowfish", - "getrandom 0.2.17", + "getrandom 0.3.4", "subtle", "zeroize", ] @@ -1208,27 +996,56 @@ dependencies = [ [[package]] name = "bincode" -version = "1.3.3" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" dependencies = [ + "bincode_derive", "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.114", ] [[package]] name = "bit-set" -version = "0.5.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bit_field" @@ -1294,7 +1111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.7.6", "cc", "cfg-if", "constant_time_eq", @@ -1310,15 +1127,6 @@ dependencies = [ "generic-array 0.14.7", ] -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array 0.14.7", -] - [[package]] name = "blowfish" version = "0.9.1" @@ -1329,6 +1137,12 @@ dependencies = [ "cipher", ] +[[package]] +name = "bnum" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f781dba93de3a5ef6dc5b17c9958b208f6f3f021623b360fb605ea51ce443f10" + [[package]] name = "bon" version = "3.8.2" @@ -1377,6 +1191,12 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "boxcar" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" + [[package]] name = "brotli" version = "8.0.2" @@ -1434,9 +1254,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -1452,41 +1272,13 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes", - "either", -] - -[[package]] -name = "bzip2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" -dependencies = [ - "bzip2-sys", -] - -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - [[package]] name = "bzip2-sys" version = "0.1.13+1.0.8" @@ -1506,15 +1298,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - [[package]] name = "cc" version = "1.2.54" @@ -1529,59 +1312,65 @@ dependencies = [ [[package]] name = "cedar-policy" -version = "2.4.2" +version = "4.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d91e3b10a0f7f2911774d5e49713c4d25753466f9e11d1cd2ec627f8a2dc857" +checksum = "c55625387d203085efb7dca8eb594188d586c99f97a83cf577d60bc588b1c705" dependencies = [ "cedar-policy-core", - "cedar-policy-validator", - "itertools 0.10.5", - "lalrpop-util", + "cedar-policy-formatter", + "itertools 0.14.0", + "linked-hash-map", + "miette", "ref-cast", + "semver", "serde", "serde_json", + "serde_with", "smol_str", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] name = "cedar-policy-core" -version = "2.4.2" +version = "4.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd2315591c6b7e18f8038f0a0529f254235fd902b6c217aabc04f2459b0d9995" +checksum = "c2ac16501266418b913a4ee73d1d14542d479c73baf7fccea2542996a2a82fe2" dependencies = [ + "chrono", + "educe", "either", - "ipnet", - "itertools 0.10.5", + "itertools 0.14.0", "lalrpop", "lalrpop-util", - "lazy_static", + "linked-hash-map", + "linked_hash_set", "miette", + "nonempty", + "ref-cast", "regex", - "rustc_lexer", + "rustc-literal-escaper", "serde", "serde_json", "serde_with", "smol_str", "stacker", - "thiserror 1.0.69", + "thiserror 2.0.18", + "unicode-security", ] [[package]] -name = "cedar-policy-validator" -version = "2.4.2" +name = "cedar-policy-formatter" +version = "4.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e756e1b2a5da742ed97e65199ad6d0893e9aa4bd6b34be1de9e70bd1e6adc7df" +checksum = "99f4b40c3d8a88264578fd9ef17b43ef35ec36e1283ae22930542dff8a6b80ae" dependencies = [ "cedar-policy-core", - "itertools 0.10.5", - "serde", - "serde_json", - "serde_with", + "itertools 0.14.0", + "logos", + "miette", + "pretty", + "regex", "smol_str", - "stacker", - "thiserror 1.0.69", - "unicode-security", ] [[package]] @@ -1596,6 +1385,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -1619,7 +1417,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1669,6 +1467,17 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "cmake" version = "0.1.57" @@ -1700,8 +1509,8 @@ version = "7.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0d05af1e006a2407bedef5af410552494ce5be9090444dbbcb57258c1af3d56" dependencies = [ - "strum 0.26.3", - "strum_macros 0.26.4", + "strum", + "strum_macros", "unicode-width 0.2.2", ] @@ -1818,15 +1627,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc32c" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" -dependencies = [ - "rustc_version", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -1842,6 +1642,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -1879,6 +1692,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-skiplist" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1891,6 +1714,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -1945,6 +1780,33 @@ dependencies = [ "memchr", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "darling" version = "0.20.11" @@ -2058,19 +1920,6 @@ dependencies = [ "serde", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core 0.9.12", -] - [[package]] name = "dashmap" version = "6.1.0" @@ -2082,7 +1931,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.12", + "parking_lot_core", ] [[package]] @@ -2093,25 +1942,23 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "datafusion" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af15bb3c6ffa33011ef579f6b0bcbe7c26584688bd6c994f548e44df67f011a" +checksum = "8ba7cb113e9c0bedf9e9765926031e132fa05a1b09ba6e93a6d1a4d7044457b8" dependencies = [ - "arrow", - "arrow-ipc", - "arrow-schema", + "arrow 57.3.0", + "arrow-schema 57.3.0", "async-trait", "bytes", - "bzip2 0.6.1", "chrono", "datafusion-catalog", "datafusion-catalog-listing", "datafusion-common", "datafusion-common-runtime", "datafusion-datasource", + "datafusion-datasource-arrow", "datafusion-datasource-csv", "datafusion-datasource-json", - "datafusion-datasource-parquet", "datafusion-execution", "datafusion-expr", "datafusion-expr-common", @@ -2128,33 +1975,30 @@ dependencies = [ "datafusion-physical-plan", "datafusion-session", "datafusion-sql", - "flate2", "futures", "itertools 0.14.0", "log", - "object_store", - "parking_lot 0.12.5", - "parquet", + "object_store 0.12.5", + "parking_lot", "rand 0.9.2", "regex", + "rstest", "sqlparser", "tempfile", "tokio", "url", "uuid", - "xz2", - "zstd", ] [[package]] name = "datafusion-catalog" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187622262ad8f7d16d3be9202b4c1e0116f1c9aa387e5074245538b755261621" +checksum = "66a3a799f914a59b1ea343906a0486f17061f39509af74e874a866428951130d" dependencies = [ - "arrow", + "arrow 57.3.0", "async-trait", - "dashmap 6.1.0", + "dashmap", "datafusion-common", "datafusion-common-runtime", "datafusion-datasource", @@ -2163,22 +2007,21 @@ dependencies = [ "datafusion-physical-expr", "datafusion-physical-plan", "datafusion-session", - "datafusion-sql", "futures", "itertools 0.14.0", "log", - "object_store", - "parking_lot 0.12.5", + "object_store 0.12.5", + "parking_lot", "tokio", ] [[package]] name = "datafusion-catalog-listing" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9657314f0a32efd0382b9a46fdeb2d233273ece64baa68a7c45f5a192daf0f83" +checksum = "6db1b113c80d7a0febcd901476a57aef378e717c54517a163ed51417d87621b0" dependencies = [ - "arrow", + "arrow 57.3.0", "async-trait", "datafusion-catalog", "datafusion-common", @@ -2186,35 +2029,33 @@ dependencies = [ "datafusion-execution", "datafusion-expr", "datafusion-physical-expr", + "datafusion-physical-expr-adapter", "datafusion-physical-expr-common", "datafusion-physical-plan", - "datafusion-session", "futures", + "itertools 0.14.0", "log", - "object_store", + "object_store 0.12.5", "tokio", ] [[package]] name = "datafusion-common" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a83760d9a13122d025fbdb1d5d5aaf93dd9ada5e90ea229add92aa30898b2d1" +checksum = "7c10f7659e96127d25e8366be7c8be4109595d6a2c3eac70421f380a7006a1b0" dependencies = [ "ahash 0.8.12", - "arrow", - "arrow-ipc", - "base64 0.22.1", + "arrow 57.3.0", + "arrow-ipc 57.3.0", "chrono", "half", "hashbrown 0.14.5", "indexmap 2.13.0", "libc", "log", - "object_store", - "parquet", + "object_store 0.12.5", "paste", - "recursive", "sqlparser", "tokio", "web-time", @@ -2222,9 +2063,9 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6234a6c7173fe5db1c6c35c01a12b2aa0f803a3007feee53483218817f8b1e" +checksum = "b92065bbc6532c6651e2f7dd30b55cba0c7a14f860c7e1d15f165c41a1868d95" dependencies = [ "futures", "log", @@ -2233,15 +2074,13 @@ dependencies = [ [[package]] name = "datafusion-datasource" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7256c9cb27a78709dd42d0c80f0178494637209cac6e29d5c93edd09b6721b86" +checksum = "fde13794244bc7581cd82f6fff217068ed79cdc344cafe4ab2c3a1c3510b38d6" dependencies = [ - "arrow", - "async-compression", + "arrow 57.3.0", "async-trait", "bytes", - "bzip2 0.6.1", "chrono", "datafusion-common", "datafusion-common-runtime", @@ -2252,126 +2091,106 @@ dependencies = [ "datafusion-physical-expr-common", "datafusion-physical-plan", "datafusion-session", - "flate2", "futures", "glob", "itertools 0.14.0", "log", - "object_store", - "parquet", + "object_store 0.12.5", "rand 0.9.2", - "tempfile", "tokio", - "tokio-util", "url", - "xz2", - "zstd", ] [[package]] -name = "datafusion-datasource-csv" -version = "50.3.0" +name = "datafusion-datasource-arrow" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64533a90f78e1684bfb113d200b540f18f268134622d7c96bbebc91354d04825" +checksum = "804fa9b4ecf3157982021770617200ef7c1b2979d57bec9044748314775a9aea" dependencies = [ - "arrow", + "arrow 57.3.0", + "arrow-ipc 57.3.0", "async-trait", "bytes", - "datafusion-catalog", "datafusion-common", "datafusion-common-runtime", "datafusion-datasource", "datafusion-execution", "datafusion-expr", - "datafusion-physical-expr", "datafusion-physical-expr-common", "datafusion-physical-plan", "datafusion-session", "futures", - "object_store", + "itertools 0.14.0", + "object_store 0.12.5", + "tokio", +] + +[[package]] +name = "datafusion-datasource-csv" +version = "51.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a1641a40b259bab38131c5e6f48fac0717bedb7dc93690e604142a849e0568" +dependencies = [ + "arrow 57.3.0", + "async-trait", + "bytes", + "datafusion-common", + "datafusion-common-runtime", + "datafusion-datasource", + "datafusion-execution", + "datafusion-expr", + "datafusion-physical-expr-common", + "datafusion-physical-plan", + "datafusion-session", + "futures", + "object_store 0.12.5", "regex", "tokio", ] [[package]] name = "datafusion-datasource-json" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d7ebeb12c77df0aacad26f21b0d033aeede423a64b2b352f53048a75bf1d6e6" +checksum = "adeacdb00c1d37271176f8fb6a1d8ce096baba16ea7a4b2671840c5c9c64fe85" dependencies = [ - "arrow", + "arrow 57.3.0", "async-trait", "bytes", - "datafusion-catalog", "datafusion-common", "datafusion-common-runtime", "datafusion-datasource", "datafusion-execution", "datafusion-expr", - "datafusion-physical-expr", "datafusion-physical-expr-common", "datafusion-physical-plan", "datafusion-session", "futures", - "object_store", - "serde_json", - "tokio", -] - -[[package]] -name = "datafusion-datasource-parquet" -version = "50.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e783c4c7d7faa1199af2df4761c68530634521b176a8d1331ddbc5a5c75133" -dependencies = [ - "arrow", - "async-trait", - "bytes", - "datafusion-catalog", - "datafusion-common", - "datafusion-common-runtime", - "datafusion-datasource", - "datafusion-execution", - "datafusion-expr", - "datafusion-functions-aggregate", - "datafusion-physical-expr", - "datafusion-physical-expr-adapter", - "datafusion-physical-expr-common", - "datafusion-physical-optimizer", - "datafusion-physical-plan", - "datafusion-pruning", - "datafusion-session", - "futures", - "itertools 0.14.0", - "log", - "object_store", - "parking_lot 0.12.5", - "parquet", - "rand 0.9.2", + "object_store 0.12.5", "tokio", ] [[package]] name = "datafusion-doc" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ee6b1d9a80d13f9deb2291f45c07044b8e62fb540dbde2453a18be17a36429" +checksum = "2b99e13947667b36ad713549237362afb054b2d8f8cc447751e23ec61202db07" [[package]] name = "datafusion-execution" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4cec0a57653bec7b933fb248d3ffa3fa3ab3bd33bd140dc917f714ac036f531" +checksum = "63695643190679037bc946ad46a263b62016931547bf119859c511f7ff2f5178" dependencies = [ - "arrow", + "arrow 57.3.0", "async-trait", - "dashmap 6.1.0", + "dashmap", "datafusion-common", "datafusion-expr", "futures", "log", - "object_store", - "parking_lot 0.12.5", + "object_store 0.12.5", + "parking_lot", "rand 0.9.2", "tempfile", "url", @@ -2379,11 +2198,11 @@ dependencies = [ [[package]] name = "datafusion-expr" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef76910bdca909722586389156d0aa4da4020e1631994d50fadd8ad4b1aa05fe" +checksum = "f9a4787cbf5feb1ab351f789063398f67654a6df75c4d37d7f637dc96f951a91" dependencies = [ - "arrow", + "arrow 57.3.0", "async-trait", "chrono", "datafusion-common", @@ -2393,19 +2212,19 @@ dependencies = [ "datafusion-functions-window-common", "datafusion-physical-expr-common", "indexmap 2.13.0", + "itertools 0.14.0", "paste", - "recursive", "serde_json", "sqlparser", ] [[package]] name = "datafusion-expr-common" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d155ccbda29591ca71a1344dd6bed26c65a4438072b400df9db59447f590bb6" +checksum = "5ce2fb1b8c15c9ac45b0863c30b268c69dc9ee7a1ee13ecf5d067738338173dc" dependencies = [ - "arrow", + "arrow 57.3.0", "datafusion-common", "indexmap 2.13.0", "itertools 0.14.0", @@ -2414,12 +2233,12 @@ dependencies = [ [[package]] name = "datafusion-functions" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de2782136bd6014670fd84fe3b0ca3b3e4106c96403c3ae05c0598577139977" +checksum = "794a9db7f7b96b3346fc007ff25e994f09b8f0511b4cf7dff651fadfe3ebb28f" dependencies = [ - "arrow", - "arrow-buffer", + "arrow 57.3.0", + "arrow-buffer 57.3.0", "base64 0.22.1", "blake2", "blake3", @@ -2434,6 +2253,7 @@ dependencies = [ "itertools 0.14.0", "log", "md-5", + "num-traits", "rand 0.9.2", "regex", "sha2", @@ -2443,12 +2263,12 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07331fc13603a9da97b74fd8a273f4238222943dffdbbed1c4c6f862a30105bf" +checksum = "1c25210520a9dcf9c2b2cbbce31ebd4131ef5af7fc60ee92b266dc7d159cb305" dependencies = [ "ahash 0.8.12", - "arrow", + "arrow 57.3.0", "datafusion-common", "datafusion-doc", "datafusion-execution", @@ -2464,12 +2284,12 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate-common" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5951e572a8610b89968a09b5420515a121fbc305c0258651f318dc07c97ab17" +checksum = "62f4a66f3b87300bb70f4124b55434d2ae3fe80455f3574701d0348da040b55d" dependencies = [ "ahash 0.8.12", - "arrow", + "arrow 57.3.0", "datafusion-common", "datafusion-expr-common", "datafusion-physical-expr-common", @@ -2477,16 +2297,17 @@ dependencies = [ [[package]] name = "datafusion-functions-nested" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdacca9302c3d8fc03f3e94f338767e786a88a33f5ebad6ffc0e7b50364b9ea3" +checksum = "ae5c06eed03918dc7fe7a9f082a284050f0e9ecf95d72f57712d1496da03b8c4" dependencies = [ - "arrow", - "arrow-ord", + "arrow 57.3.0", + "arrow-ord 57.3.0", "datafusion-common", "datafusion-doc", "datafusion-execution", "datafusion-expr", + "datafusion-expr-common", "datafusion-functions", "datafusion-functions-aggregate", "datafusion-functions-aggregate-common", @@ -2499,27 +2320,27 @@ dependencies = [ [[package]] name = "datafusion-functions-table" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37ff8a99434fbbad604a7e0669717c58c7c4f14c472d45067c4b016621d981" +checksum = "db4fed1d71738fbe22e2712d71396db04c25de4111f1ec252b8f4c6d3b25d7f5" dependencies = [ - "arrow", + "arrow 57.3.0", "async-trait", "datafusion-catalog", "datafusion-common", "datafusion-expr", "datafusion-physical-plan", - "parking_lot 0.12.5", + "parking_lot", "paste", ] [[package]] name = "datafusion-functions-window" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e2aea7c79c926cffabb13dc27309d4eaeb130f4a21c8ba91cdd241c813652b" +checksum = "1d92206aa5ae21892f1552b4d61758a862a70956e6fd7a95cb85db1de74bc6d1" dependencies = [ - "arrow", + "arrow 57.3.0", "datafusion-common", "datafusion-doc", "datafusion-expr", @@ -2533,9 +2354,9 @@ dependencies = [ [[package]] name = "datafusion-functions-window-common" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fead257ab5fd2ffc3b40fda64da307e20de0040fe43d49197241d9de82a487f" +checksum = "53ae9bcc39800820d53a22d758b3b8726ff84a5a3e24cecef04ef4e5fdf1c7cc" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -2543,22 +2364,22 @@ dependencies = [ [[package]] name = "datafusion-macros" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec6f637bce95efac05cdfb9b6c19579ed4aa5f6b94d951cfa5bb054b7bb4f730" +checksum = "1063ad4c9e094b3f798acee16d9a47bd7372d9699be2de21b05c3bd3f34ab848" dependencies = [ - "datafusion-expr", + "datafusion-doc", "quote", "syn 2.0.114", ] [[package]] name = "datafusion-optimizer" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6583ef666ae000a613a837e69e456681a9faa96347bf3877661e9e89e141d8a" +checksum = "9f35f9ec5d08b87fd1893a30c2929f2559c2f9806ca072d8fefca5009dc0f06a" dependencies = [ - "arrow", + "arrow 57.3.0", "chrono", "datafusion-common", "datafusion-expr", @@ -2567,19 +2388,18 @@ dependencies = [ "indexmap 2.13.0", "itertools 0.14.0", "log", - "recursive", "regex", "regex-syntax", ] [[package]] name = "datafusion-physical-expr" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8668103361a272cbbe3a61f72eca60c9b7c706e87cc3565bcf21e2b277b84f6" +checksum = "c30cc8012e9eedcb48bbe112c6eff4ae5ed19cf3003cb0f505662e88b7014c5d" dependencies = [ "ahash 0.8.12", - "arrow", + "arrow 57.3.0", "datafusion-common", "datafusion-expr", "datafusion-expr-common", @@ -2589,19 +2409,18 @@ dependencies = [ "hashbrown 0.14.5", "indexmap 2.13.0", "itertools 0.14.0", - "log", - "parking_lot 0.12.5", + "parking_lot", "paste", "petgraph 0.8.3", ] [[package]] name = "datafusion-physical-expr-adapter" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "815acced725d30601b397e39958e0e55630e0a10d66ef7769c14ae6597298bb0" +checksum = "7f9ff2dbd476221b1f67337699eff432781c4e6e1713d2aefdaa517dfbf79768" dependencies = [ - "arrow", + "arrow 57.3.0", "datafusion-common", "datafusion-expr", "datafusion-functions", @@ -2612,12 +2431,12 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6652fe7b5bf87e85ed175f571745305565da2c0b599d98e697bcbedc7baa47c3" +checksum = "90da43e1ec550b172f34c87ec68161986ced70fd05c8d2a2add66eef9c276f03" dependencies = [ "ahash 0.8.12", - "arrow", + "arrow 57.3.0", "datafusion-common", "datafusion-expr-common", "hashbrown 0.14.5", @@ -2626,11 +2445,11 @@ dependencies = [ [[package]] name = "datafusion-physical-optimizer" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b7d623eb6162a3332b564a0907ba00895c505d101b99af78345f1acf929b5c" +checksum = "ce9804f799acd7daef3be7aaffe77c0033768ed8fdbf5fb82fc4c5f2e6bc14e6" dependencies = [ - "arrow", + "arrow 57.3.0", "datafusion-common", "datafusion-execution", "datafusion-expr", @@ -2640,20 +2459,18 @@ dependencies = [ "datafusion-physical-plan", "datafusion-pruning", "itertools 0.14.0", - "log", - "recursive", ] [[package]] name = "datafusion-physical-plan" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2f7f778a1a838dec124efb96eae6144237d546945587557c9e6936b3414558c" +checksum = "0acf0ad6b6924c6b1aa7d213b181e012e2d3ec0a64ff5b10ee6282ab0f8532ac" dependencies = [ "ahash 0.8.12", - "arrow", - "arrow-ord", - "arrow-schema", + "arrow 57.3.0", + "arrow-ord 57.3.0", + "arrow-schema 57.3.0", "async-trait", "chrono", "datafusion-common", @@ -2670,19 +2487,18 @@ dependencies = [ "indexmap 2.13.0", "itertools 0.14.0", "log", - "parking_lot 0.12.5", + "parking_lot", "pin-project-lite", "tokio", ] [[package]] name = "datafusion-pruning" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1e59e2ca14fe3c30f141600b10ad8815e2856caa59ebbd0e3e07cd3d127a65" +checksum = "ac2c2498a1f134a9e11a9f5ed202a2a7d7e9774bd9249295593053ea3be999db" dependencies = [ - "arrow", - "arrow-schema", + "arrow 57.3.0", "datafusion-common", "datafusion-datasource", "datafusion-expr-common", @@ -2695,41 +2511,31 @@ dependencies = [ [[package]] name = "datafusion-session" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21ef8e2745583619bd7a49474e8f45fbe98ebb31a133f27802217125a7b3d58d" +checksum = "8f96eebd17555386f459037c65ab73aae8df09f464524c709d6a3134ad4f4776" dependencies = [ - "arrow", "async-trait", - "dashmap 6.1.0", "datafusion-common", - "datafusion-common-runtime", "datafusion-execution", "datafusion-expr", - "datafusion-physical-expr", "datafusion-physical-plan", - "datafusion-sql", - "futures", - "itertools 0.14.0", - "log", - "object_store", - "parking_lot 0.12.5", - "tokio", + "parking_lot", ] [[package]] name = "datafusion-sql" -version = "50.3.0" +version = "51.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89abd9868770386fede29e5a4b14f49c0bf48d652c3b9d7a8a0332329b87d50b" +checksum = "3fc195fe60634b2c6ccfd131b487de46dc30eccae8a3c35a13f136e7f440414f" dependencies = [ - "arrow", + "arrow 57.3.0", "bigdecimal", + "chrono", "datafusion-common", "datafusion-expr", "indexmap 2.13.0", "log", - "recursive", "regex", "sqlparser", ] @@ -2833,16 +2639,6 @@ dependencies = [ "dirs-sys", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - [[package]] name = "dirs-sys" version = "0.5.0" @@ -2851,21 +2647,10 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.2", + "redox_users", "windows-sys 0.61.2", ] -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -2877,15 +2662,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - [[package]] name = "dmp" version = "0.2.3" @@ -2896,12 +2672,6 @@ dependencies = [ "urlencoding", ] -[[package]] -name = "double-ended-peekable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d05e1c0dbad51b52c38bda7adceef61b9efc2baf04acfe8726a8c4630a6f57" - [[package]] name = "downcast-rs" version = "2.0.2" @@ -2945,12 +2715,84 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array 0.14.7", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "ena" version = "0.14.3" @@ -2977,9 +2819,29 @@ dependencies = [ [[package]] name = "endian-type" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2" + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] [[package]] name = "env_home" @@ -3098,9 +2960,9 @@ checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" [[package]] name = "fastembed" -version = "5.8.1" +version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a3f841f27a44bcc32214f8df75cc9b6cea55dbbebbfe546735690eab5bb2d2" +checksum = "b4339d45a80579ab8305616a501eacdbf18fb0f7def7fa6e4c0b75941416d5b0" dependencies = [ "anyhow", "hf-hub", @@ -3113,6 +2975,17 @@ dependencies = [ "tokenizers", ] +[[package]] +name = "fastnum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4089ab2dfd45d8ddc92febb5ca80644389d5ebb954f40231274a3f18341762e2" +dependencies = [ + "bnum", + "num-integer", + "num-traits", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -3148,18 +3021,28 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "fixedbitset" version = "0.5.7" @@ -3174,6 +3057,7 @@ checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" dependencies = [ "bitflags 2.10.0", "rustc_version", + "serde", ] [[package]] @@ -3184,7 +3068,6 @@ checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "miniz_oxide", - "zlib-rs", ] [[package]] @@ -3262,12 +3145,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] -name = "fsst" -version = "1.0.1" +name = "fsevent-sys" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffdff7a2d68d22afc0657eddde3e946371ce7cfe730a3f78a5ed44ea5b1cb2e" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" dependencies = [ - "arrow-array", + "libc", +] + +[[package]] +name = "fsst" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f03a771ab914e207dd26bd2f12666839555ec8ecc7e1770e1ed6f9900d899a4" +dependencies = [ + "arrow-array 57.3.0", "rand 0.9.2", ] @@ -3344,19 +3236,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.31" @@ -3380,6 +3259,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -3407,15 +3292,6 @@ dependencies = [ "thread_local", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "generator" version = "0.8.8" @@ -3427,7 +3303,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link", + "windows-link 0.2.1", "windows-result 0.4.1", ] @@ -3457,24 +3333,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", -] - -[[package]] -name = "geo" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f811f663912a69249fa620dcd2a005db7254529da2d8a0b23942e81f47084501" -dependencies = [ - "earcutr", - "float_next_after", - "geo-types", - "geographiclib-rs", - "log", - "num-traits", - "robust", - "rstar 0.12.2", - "serde", - "spade", + "zeroize", ] [[package]] @@ -3492,6 +3351,28 @@ dependencies = [ "num-traits", "robust", "rstar 0.12.2", + "serde", + "spade", +] + +[[package]] +name = "geo" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3901269ec6d4f6068d3f09e5f02f995bd076398dcd1dfec407cd230b02d11b" +dependencies = [ + "earcutr", + "float_next_after", + "geo-types", + "geographiclib-rs", + "i_overlay", + "log", + "num-traits", + "rand 0.8.5", + "robust", + "rstar 0.12.2", + "serde", + "sif-itree", "spade", ] @@ -3510,7 +3391,7 @@ version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24f8647af4005fa11da47cd56252c6ef030be8fa97bdbf355e7dfb6348f0a82c" dependencies = [ - "approx 0.5.1", + "approx", "num-traits", "rayon", "rstar 0.10.0", @@ -3523,13 +3404,13 @@ dependencies = [ [[package]] name = "geoarrow-array" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d1884b17253d8572e88833c282fcbb442365e4ae5f9052ced2831608253436c" +checksum = "dc1cc4106ac0a0a512c398961ce95d8150475c84a84e17c4511c3643fa120a17" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-schema", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-schema 57.3.0", "geo-traits", "geoarrow-schema", "num-traits", @@ -3539,12 +3420,12 @@ dependencies = [ [[package]] name = "geoarrow-expr-geo" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a67d3b543bc3ebeffdc204b67d69b8f9fcd33d76269ddd4a4618df99f053a934" +checksum = "fa84300361ce57fb875bcaa6e32b95b0aff5c6b1af692b936bdd58ff343f4394" dependencies = [ - "arrow-array", - "arrow-buffer", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", "geo 0.31.0", "geo-traits", "geoarrow-array", @@ -3553,11 +3434,11 @@ dependencies = [ [[package]] name = "geoarrow-schema" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f1b18b1c9a44ecd72be02e53d6e63bbccfdc8d1765206226af227327e2be6e" +checksum = "e97be4e9f523f92bd6a0e0458323f4b783d073d011664decd8dbf05651704f34" dependencies = [ - "arrow-schema", + "arrow-schema 57.3.0", "geo-traits", "serde", "serde_json", @@ -3566,13 +3447,13 @@ dependencies = [ [[package]] name = "geodatafusion" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d676b8d8b5f391ab4270ba31e9b599ee2c3d780405a38e272a0a7565ea189c" +checksum = "773cfa1fb0d7f7661b76b3fde00f3ffd8e0ff7b3635096f0ff6294fe5ca62a2b" dependencies = [ - "arrow-arith", - "arrow-array", - "arrow-schema", + "arrow-arith 57.3.0", + "arrow-array 57.3.0", + "arrow-schema 57.3.0", "datafusion", "geo 0.31.0", "geo-traits", @@ -3630,6 +3511,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "gif" version = "0.14.1" @@ -3647,35 +3541,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] -name = "gloo-timers" -version = "0.3.0" +name = "group" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", + "ff", + "rand_core 0.6.4", + "subtle", ] [[package]] -name = "h2" -version = "0.3.27" +name = "guardian" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.13.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] +checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" [[package]] name = "h2" @@ -3688,7 +3568,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http", "indexmap 2.13.0", "slab", "tokio", @@ -3778,6 +3658,30 @@ dependencies = [ "serde_core", ] +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heapless" version = "0.6.1" @@ -3838,7 +3742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97" dependencies = [ "dirs", - "http 1.4.0", + "http", "indicatif", "libc", "log", @@ -3852,6 +3756,15 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -3867,15 +3780,6 @@ version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0f0ae375a85536cac3a243e3a9cda80a47910348abdea7e2c22f8ec556d586d" -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "html5ever" version = "0.35.0" @@ -3893,17 +3797,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.4.0" @@ -3914,17 +3807,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -3932,7 +3814,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http", ] [[package]] @@ -3943,8 +3825,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -3976,30 +3858,6 @@ dependencies = [ "serde", ] -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.8.1" @@ -4010,10 +3868,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.13", - "http 1.4.0", - "http-body 1.0.1", + "h2", + "http", + "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -4022,37 +3881,34 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls 0.21.12", - "tokio", - "tokio-rustls 0.24.1", -] - [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.4.0", - "hyper 1.8.1", + "http", + "hyper", "hyper-util", - "rustls 0.23.36", - "rustls-native-certs", + "rustls", + "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", "tower-service", - "webpki-roots 1.0.5", ] [[package]] @@ -4063,7 +3919,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "native-tls", "tokio", @@ -4082,14 +3938,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "hyper 1.8.1", + "http", + "http-body", + "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2", "system-configuration", "tokio", "tower-service", @@ -4254,6 +4110,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -4357,30 +4219,40 @@ dependencies = [ "web-time", ] +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.10.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "inout" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "block-padding", "generic-array 0.14.7", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "integer-encoding" -version = "3.0.4" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" +checksum = "14c00403deb17c3221a1fe4fb571b9ed0370b3dcd116553c77fa294a3d918699" [[package]] name = "interpolate_name" @@ -4409,15 +4281,6 @@ dependencies = [ "serde", ] -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.11.0" @@ -4547,7 +4410,7 @@ dependencies = [ "jiff", "nom 8.0.0", "num-traits", - "ordered-float 5.1.0", + "ordered-float", "rand 0.9.2", "ryu", "serde", @@ -4556,74 +4419,111 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "9.3.1" +version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ + "aws-lc-rs", "base64 0.22.1", + "ed25519-dalek", + "getrandom 0.2.17", + "hmac", "js-sys", + "p256", + "p384", "pem", - "ring", + "rand 0.8.5", + "rsa", "serde", "serde_json", + "sha2", + "signature", "simple_asn1", ] [[package]] -name = "lalrpop" -version = "0.20.2" +name = "keccak" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lalrpop" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" dependencies = [ "ascii-canvas", "bit-set", "ena", - "itertools 0.11.0", + "itertools 0.14.0", "lalrpop-util", - "petgraph 0.6.5", + "petgraph 0.7.1", "pico-args", "regex", "regex-syntax", + "sha3", "string_cache", "term", - "tiny-keccak", "unicode-xid", "walkdir", ] [[package]] name = "lalrpop-util" -version = "0.20.2" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" +checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" dependencies = [ "regex-automata", + "rustversion", ] [[package]] name = "lance" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c439decbc304e180748e34bb6d3df729069a222e83e74e2185c38f107136e9" +checksum = "47b685aca3f97ee02997c83ded16f59c747ccb69e74c8abbbae4aa3d22cf1301" dependencies = [ - "arrow", - "arrow-arith", - "arrow-array", - "arrow-buffer", - "arrow-ipc", - "arrow-ord", - "arrow-row", - "arrow-schema", - "arrow-select", + "arrow 57.3.0", + "arrow-arith 57.3.0", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-ipc 57.3.0", + "arrow-ord 57.3.0", + "arrow-row 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", "async-recursion", "async-trait", "async_cell", - "aws-credential-types", - "aws-sdk-dynamodb", "byteorder", "bytes", "chrono", - "dashmap 6.1.0", + "dashmap", "datafusion", "datafusion-expr", "datafusion-functions", @@ -4648,13 +4548,13 @@ dependencies = [ "lance-table", "log", "moka", - "object_store", + "object_store 0.12.5", "permutation", "pin-project", "prost", "prost-types", "rand 0.9.2", - "roaring", + "roaring 0.10.9", "semver", "serde", "serde_json", @@ -4669,16 +4569,17 @@ dependencies = [ [[package]] name = "lance-arrow" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ee5508b225456d3d56998eaeef0d8fbce5ea93856df47b12a94d2e74153210" +checksum = "daf00c7537df524cc518a089f0d156a036d95ca3f5bc2bc1f0a9f9293e9b62ef" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-cast 57.3.0", + "arrow-data 57.3.0", + "arrow-ord 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", "bytes", "getrandom 0.2.17", "half", @@ -4689,9 +4590,9 @@ dependencies = [ [[package]] name = "lance-bitpacking" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1c065fb3bd4a8cc4f78428443e990d4921aa08f707b676753db740e0b402a21" +checksum = "46752e4ac8fc5590a445e780b63a8800adc7a770bd74770a8dc66963778e4e77" dependencies = [ "arrayref", "paste", @@ -4700,13 +4601,13 @@ dependencies = [ [[package]] name = "lance-core" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8856abad92e624b75cd57a04703f6441948a239463bdf973f2ac1924b0bcdbe" +checksum = "3d13d87d07305c6d4b4dc7780fb1107babf782a0e5b1dc7872e17ae1f8fd11ca" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-schema", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-schema 57.3.0", "async-trait", "byteorder", "bytes", @@ -4715,17 +4616,18 @@ dependencies = [ "datafusion-sql", "deepsize", "futures", + "itertools 0.13.0", "lance-arrow", "libc", "log", "mock_instant", "moka", "num_cpus", - "object_store", + "object_store 0.12.5", "pin-project", "prost", "rand 0.9.2", - "roaring", + "roaring 0.10.9", "serde_json", "snafu", "tempfile", @@ -4738,16 +4640,16 @@ dependencies = [ [[package]] name = "lance-datafusion" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8835308044cef5467d7751be87fcbefc2db01c22370726a8704bd62991693f" +checksum = "6451b5af876eaef8bec4b38a39dadac9d44621e1ecf85d0cdf6097a5d0aa8721" dependencies = [ - "arrow", - "arrow-array", - "arrow-buffer", - "arrow-ord", - "arrow-schema", - "arrow-select", + "arrow 57.3.0", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-ord 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", "async-trait", "chrono", "datafusion", @@ -4770,36 +4672,37 @@ dependencies = [ [[package]] name = "lance-datagen" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612de1e888bb36f6bf51196a6eb9574587fdf256b1759a4c50e643e00d5f96d0" +checksum = "e1736708dd7867dfbab8fcc930b21c96717c6c00be73b7d9a240336a4ed80375" dependencies = [ - "arrow", - "arrow-array", - "arrow-cast", - "arrow-schema", + "arrow 57.3.0", + "arrow-array 57.3.0", + "arrow-cast 57.3.0", + "arrow-schema 57.3.0", "chrono", "futures", "half", "hex", "rand 0.9.2", + "rand_distr 0.5.1", "rand_xoshiro", "random_word", ] [[package]] name = "lance-encoding" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b456b29b135d3c7192602e516ccade38b5483986e121895fa43cf1fdb38bf60" +checksum = "d6b6ca4ff94833240d5ba4a94a742cba786d1949b3c3fa7e11d6f0050443432a" dependencies = [ - "arrow-arith", - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow-arith 57.3.0", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-cast 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", "bytemuck", "byteorder", "bytes", @@ -4819,7 +4722,7 @@ dependencies = [ "prost-types", "rand 0.9.2", "snafu", - "strum 0.26.3", + "strum", "tokio", "tracing", "xxhash-rust", @@ -4828,16 +4731,16 @@ dependencies = [ [[package]] name = "lance-file" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab1538d14d5bb3735b4222b3f5aff83cfa59cc6ef7cdd3dd9139e4c77193c80b" +checksum = "55fbe959bffe185543aed3cbeb14484f1aa2e55886034fdb1ea3d8cc9b70aad8" dependencies = [ - "arrow-arith", - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow-arith 57.3.0", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", "async-recursion", "async-trait", "byteorder", @@ -4851,7 +4754,7 @@ dependencies = [ "lance-io", "log", "num-traits", - "object_store", + "object_store 0.12.5", "prost", "prost-build", "prost-types", @@ -4862,29 +4765,32 @@ dependencies = [ [[package]] name = "lance-geo" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5a69a2f3b55703d9c240ad7c5ffa2c755db69e9cf8aa05efe274a212910472d" +checksum = "a52b0adabc953d457f336a784a3b37353a180e6a79905f544949746e0d4c6483" dependencies = [ "datafusion", + "geo-traits", "geo-types", "geoarrow-array", "geoarrow-schema", "geodatafusion", + "lance-core", + "serde", ] [[package]] name = "lance-index" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea84613df6fa6b9168a1f056ba4f9cb73b90a1b452814c6fd4b3529bcdbfc78" +checksum = "6b67654bf86fd942dd2cf08294ee7e91053427cd148225f49c9ff398ff9a40fd" dependencies = [ - "arrow", - "arrow-arith", - "arrow-array", - "arrow-ord", - "arrow-schema", - "arrow-select", + "arrow 57.3.0", + "arrow-arith 57.3.0", + "arrow-array 57.3.0", + "arrow-ord 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", "async-channel", "async-recursion", "async-trait", @@ -4901,6 +4807,9 @@ dependencies = [ "dirs", "fst", "futures", + "geo-types", + "geoarrow-array", + "geoarrow-schema", "half", "itertools 0.13.0", "jsonb", @@ -4910,6 +4819,7 @@ dependencies = [ "lance-datagen", "lance-encoding", "lance-file", + "lance-geo", "lance-io", "lance-linalg", "lance-table", @@ -4917,16 +4827,18 @@ dependencies = [ "log", "ndarray 0.16.1", "num-traits", - "object_store", + "object_store 0.12.5", "prost", "prost-build", "prost-types", "rand 0.9.2", "rand_distr 0.5.1", + "rangemap", "rayon", - "roaring", + "roaring 0.10.9", "serde", "serde_json", + "smallvec", "snafu", "tantivy", "tempfile", @@ -4938,22 +4850,20 @@ dependencies = [ [[package]] name = "lance-io" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3fc4c1d941fceef40a0edbd664dbef108acfc5d559bb9e7f588d0c733cbc35" +checksum = "8eb0ccc1c414e31687d83992d546af0a0237c8d2f4bf2ae3d347d539fd0fc141" dependencies = [ - "arrow", - "arrow-arith", - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow 57.3.0", + "arrow-arith 57.3.0", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-cast 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", "async-recursion", "async-trait", - "aws-config", - "aws-credential-types", "byteorder", "bytes", "chrono", @@ -4963,9 +4873,7 @@ dependencies = [ "lance-core", "lance-namespace", "log", - "object_store", - "object_store_opendal", - "opendal", + "object_store 0.12.5", "path_abs", "pin-project", "prost", @@ -4980,13 +4888,13 @@ dependencies = [ [[package]] name = "lance-linalg" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ffbc5ce367fbf700a69de3fe0612ee1a11191a64a632888610b6bacfa0f63" +checksum = "083404cf12dcdb1a7df98fb58f9daf626b6e43a2f794b37b6b89b4012a0e1f78" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-schema", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-schema 57.3.0", "cc", "deepsize", "half", @@ -4998,11 +4906,11 @@ dependencies = [ [[package]] name = "lance-namespace" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791bbcd868ee758123a34e07d320a1fb99379432b5ecc0e78d6b4686e999b629" +checksum = "c12778d2aabf9c2bfd16e2509ebe120e562a288d8ae630ec6b6b204868df41b2" dependencies = [ - "arrow", + "arrow 57.3.0", "async-trait", "bytes", "lance-core", @@ -5012,15 +4920,16 @@ dependencies = [ [[package]] name = "lance-namespace-impls" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee713505576f6b1988a491f77c7ca8b0cf7090a393598e63c85079fa70a53ebf" +checksum = "8863aababdd13a6d2c8d6179dc6981f4f8f49d8b66a00c5dd75115aec4cadc99" dependencies = [ - "arrow", - "arrow-ipc", - "arrow-schema", + "arrow 57.3.0", + "arrow-ipc 57.3.0", + "arrow-schema 57.3.0", "async-trait", "bytes", + "chrono", "futures", "lance", "lance-core", @@ -5028,7 +4937,7 @@ dependencies = [ "lance-io", "lance-namespace", "log", - "object_store", + "object_store 0.12.5", "rand 0.9.2", "serde_json", "snafu", @@ -5038,9 +4947,9 @@ dependencies = [ [[package]] name = "lance-namespace-reqwest-client" -version = "0.0.18" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea349999bcda4eea53fc05d334b3775ec314761e6a706555c777d7a29b18d19" +checksum = "a2acdba67f84190067532fce07b51a435dd390d7cdc1129a05003e5cb3274cf0" dependencies = [ "reqwest 0.12.28", "serde", @@ -5051,18 +4960,16 @@ dependencies = [ [[package]] name = "lance-table" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fdb2d56bfa4d1511c765fa0cc00fdaa37e5d2d1cd2f57b3c6355d9072177052" +checksum = "f0fcc83f197ce2000c4abe4f5e0873490ab1f41788fa76571c4209b87d4daf50" dependencies = [ - "arrow", - "arrow-array", - "arrow-buffer", - "arrow-ipc", - "arrow-schema", + "arrow 57.3.0", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-ipc 57.3.0", + "arrow-schema 57.3.0", "async-trait", - "aws-credential-types", - "aws-sdk-dynamodb", "byteorder", "bytes", "chrono", @@ -5073,13 +4980,13 @@ dependencies = [ "lance-file", "lance-io", "log", - "object_store", + "object_store 0.12.5", "prost", "prost-build", "prost-types", "rand 0.9.2", "rangemap", - "roaring", + "roaring 0.10.9", "semver", "serde", "serde_json", @@ -5092,12 +4999,12 @@ dependencies = [ [[package]] name = "lance-testing" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8ccb1a4a9284435c6a8c02c8c06e7e041bece0d7f722152159353cf55dc51e3" +checksum = "7fb1f7c7e06f91360e141ecee1cf2110f858c231705f69f2cd2fda9e30c1e9f4" dependencies = [ - "arrow-array", - "arrow-schema", + "arrow-array 57.3.0", + "arrow-schema 57.3.0", "lance-arrow", "num-traits", "rand 0.9.2", @@ -5105,19 +5012,19 @@ dependencies = [ [[package]] name = "lancedb" -version = "0.23.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9217d7d3a1f4e088bdedaad9b4fa79045b077e07f961f1cd3ec6f90850c425f2" +checksum = "23f89be66655e426c6afac36032ede6d7213248462329dd73f9ac6bd1cc370b8" dependencies = [ "ahash 0.8.12", - "arrow", - "arrow-array", - "arrow-cast", - "arrow-data", - "arrow-ipc", - "arrow-ord", - "arrow-schema", - "arrow-select", + "arrow 57.3.0", + "arrow-array 57.3.0", + "arrow-cast 57.3.0", + "arrow-data 57.3.0", + "arrow-ipc 57.3.0", + "arrow-ord 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", "async-trait", "bytes", "chrono", @@ -5126,6 +5033,7 @@ dependencies = [ "datafusion-common", "datafusion-execution", "datafusion-expr", + "datafusion-physical-expr", "datafusion-physical-plan", "futures", "half", @@ -5147,7 +5055,7 @@ dependencies = [ "log", "moka", "num-traits", - "object_store", + "object_store 0.12.5", "pin-project", "rand 0.9.2", "regex", @@ -5171,6 +5079,12 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lebe" version = "0.5.3" @@ -5242,19 +5156,13 @@ dependencies = [ [[package]] name = "lexicmp" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7378d131ddf24063b32cbd7e91668d183140c4b3906270635a4d633d1068ea5d" +checksum = "0e8f89da8fd95c4eb6274e914694bea90c7826523b26f2a2fd863d44b9d42c43" dependencies = [ - "any_ascii", + "deunicode", ] -[[package]] -name = "libbz2-rs-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" - [[package]] name = "libc" version = "0.2.180" @@ -5271,6 +5179,16 @@ dependencies = [ "cc", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libm" version = "0.2.15" @@ -5288,15 +5206,32 @@ dependencies = [ ] [[package]] -name = "linfa-linalg" -version = "0.1.0" +name = "libz-sys" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e7562b41c8876d3367897067013bb2884cc78e6893f092ecd26b305176ac82" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ - "ndarray 0.15.6", - "num-traits", - "rand 0.8.5", - "thiserror 1.0.69", + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +dependencies = [ + "serde", +] + +[[package]] +name = "linked_hash_set" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" +dependencies = [ + "linked-hash-map", ] [[package]] @@ -5332,6 +5267,38 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "logos" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" +dependencies = [ + "fnv", + "proc-macro2", + "quote", + "regex-automata", + "regex-syntax", + "syn 2.0.114", +] + +[[package]] +name = "logos-derive" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" +dependencies = [ + "logos-codegen", +] + [[package]] name = "loom" version = "0.7.2" @@ -5393,6 +5360,12 @@ name = "lz4_flex" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" + +[[package]] +name = "lz4_flex" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab6473172471198271ff72e9379150e9dfd70d8e533e0752a27e515b48dd375e" dependencies = [ "twox-hash", ] @@ -5403,17 +5376,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1670343e58806300d87950e3401e820b519b9384281bbabfb15e3636689ffd69" -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "mac" version = "0.1.1" @@ -5473,6 +5435,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "matrixmultiply" version = "0.3.10" @@ -5532,21 +5500,21 @@ dependencies = [ [[package]] name = "miette" -version = "5.10.0" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" dependencies = [ + "cfg-if", "miette-derive", - "once_cell", - "thiserror 1.0.69", + "serde", "unicode-width 0.1.14", ] [[package]] name = "miette-derive" -version = "5.10.0" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", @@ -5592,6 +5560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -5615,7 +5584,7 @@ dependencies = [ "equivalent", "event-listener", "futures-util", - "parking_lot 0.12.5", + "parking_lot", "portable-atomic", "smallvec", "tagptr", @@ -5654,23 +5623,6 @@ dependencies = [ "pxfm", ] -[[package]] -name = "multer" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http 1.4.0", - "httparse", - "memchr", - "mime", - "spin", - "version_check", -] - [[package]] name = "multimap" version = "0.10.1" @@ -5683,15 +5635,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" -[[package]] -name = "nanoid" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" -dependencies = [ - "rand 0.8.5", -] - [[package]] name = "native-tls" version = "0.2.14" @@ -5710,17 +5653,15 @@ dependencies = [ ] [[package]] -name = "ndarray" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +name = "ncl-import-resolver" +version = "0.1.0" dependencies = [ - "approx 0.4.0", - "matrixmultiply", - "num-complex", - "num-integer", - "num-traits", - "rawpointer", + "anyhow", + "dirs", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", ] [[package]] @@ -5755,13 +5696,13 @@ dependencies = [ [[package]] name = "ndarray-stats" -version = "0.5.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af5a8477ac96877b5bd1fd67e0c28736c12943aba24eda92b127e036b0c8f400" +checksum = "9b6e54a8b65764f71827a90ca1d56965ec0c67f069f996477bd493402a901d1f" dependencies = [ - "indexmap 1.9.3", - "itertools 0.10.5", - "ndarray 0.15.6", + "indexmap 2.13.0", + "itertools 0.13.0", + "ndarray 0.17.2", "noisy_float", "num-integer", "num-traits", @@ -5783,6 +5724,21 @@ dependencies = [ "smallvec", ] +[[package]] +name = "nkeys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879011babc47a1c7fdf5a935ae3cfe94f34645ca0cac1c7f6424b36fc743d1bf" +dependencies = [ + "data-encoding", + "ed25519", + "ed25519-dalek", + "getrandom 0.2.17", + "log", + "rand 0.8.5", + "signatory", +] + [[package]] name = "noisy_float" version = "0.2.1" @@ -5811,12 +5767,48 @@ dependencies = [ "memchr", ] +[[package]] +name = "nonempty" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" +dependencies = [ + "serde", +] + [[package]] name = "noop_proc_macro" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.10.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "ntapi" version = "0.4.2" @@ -5835,6 +5827,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nuid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83" +dependencies = [ + "rand 0.8.5", +] + [[package]] name = "num" version = "0.4.3" @@ -5980,6 +5981,25 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.32.2" @@ -5996,28 +6016,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbfbfff40aeccab00ec8a910b57ca8ecf4319b335c542f2edcd19dd25a1e2a00" dependencies = [ "async-trait", - "base64 0.22.1", "bytes", "chrono", - "form_urlencoded", "futures", - "http 1.4.0", - "http-body-util", - "httparse", + "http", "humantime", - "hyper 1.8.1", "itertools 0.14.0", - "md-5", - "parking_lot 0.12.5", + "parking_lot", "percent-encoding", - "quick-xml 0.38.4", - "rand 0.9.2", - "reqwest 0.12.28", - "ring", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", "thiserror 2.0.18", "tokio", "tracing", @@ -6028,19 +6034,27 @@ dependencies = [ ] [[package]] -name = "object_store_opendal" -version = "0.55.0" +name = "object_store" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113ab0769e972eee585e57407b98de08bda5354fa28e8ba4d89038d6cb6a8991" +checksum = "c2858065e55c148d294a9f3aae3b0fa9458edadb41a108397094566f4e3c0dfb" dependencies = [ "async-trait", "bytes", "chrono", "futures", - "object_store", - "opendal", - "pin-project", + "http", + "humantime", + "itertools 0.14.0", + "parking_lot", + "percent-encoding", + "thiserror 2.0.18", "tokio", + "tracing", + "url", + "walkdir", + "wasm-bindgen-futures", + "web-time", ] [[package]] @@ -6077,36 +6091,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "opendal" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d075ab8a203a6ab4bc1bce0a4b9fe486a72bf8b939037f4b78d95386384bc80a" -dependencies = [ - "anyhow", - "backon", - "base64 0.22.1", - "bytes", - "crc32c", - "futures", - "getrandom 0.2.17", - "http 1.4.0", - "http-body 1.0.1", - "jiff", - "log", - "md-5", - "percent-encoding", - "quick-xml 0.38.4", - "reqsign", - "reqwest 0.12.28", - "serde", - "serde_json", - "sha2", - "tokio", - "url", - "uuid", -] - [[package]] name = "openssl" version = "0.10.75" @@ -6163,15 +6147,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-float" version = "5.1.0" @@ -6181,16 +6156,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - [[package]] name = "ort" version = "2.0.0-rc.11" @@ -6215,12 +6180,6 @@ dependencies = [ "ureq 3.1.4", ] -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - [[package]] name = "ownedbytes" version = "0.9.0" @@ -6230,23 +6189,46 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "papaya" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92dd0b07c53a0a0c764db2ace8c541dc47320dad97c2200c2a637ab9dd2328f" +dependencies = [ + "equivalent", + "seize", +] + [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - [[package]] name = "parking_lot" version = "0.12.5" @@ -6254,21 +6236,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core 0.9.12", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", + "parking_lot_core", ] [[package]] @@ -6279,46 +6247,9 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", - "windows-link", -] - -[[package]] -name = "parquet" -version = "56.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dbd48ad52d7dccf8ea1b90a3ddbfaea4f69878dd7683e51c507d4bc52b5b27" -dependencies = [ - "ahash 0.8.12", - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-data", - "arrow-ipc", - "arrow-schema", - "arrow-select", - "base64 0.22.1", - "brotli", - "bytes", - "chrono", - "flate2", - "futures", - "half", - "hashbrown 0.16.1", - "lz4_flex", - "num", - "num-bigint", - "object_store", - "paste", - "ring", - "seq-macro", - "simdutf8", - "snap", - "thrift", - "tokio", - "twox-hash", - "zstd", + "windows-link 0.2.1", ] [[package]] @@ -6411,33 +6342,13 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df202b0b0f5b8e389955afd5f27b007b00fb948162953f1db9c70d2c7e3157d7" -[[package]] -name = "pest" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset 0.4.2", - "indexmap 2.13.0", -] - [[package]] name = "petgraph" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ - "fixedbitset 0.5.7", + "fixedbitset", "indexmap 2.13.0", ] @@ -6447,29 +6358,19 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ - "fixedbitset 0.5.7", + "fixedbitset", "hashbrown 0.15.5", "indexmap 2.13.0", "serde", ] -[[package]] -name = "pharos" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" -dependencies = [ - "futures", - "rustc_version", -] - [[package]] name = "phf" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros", + "phf_macros 0.11.3", "phf_shared 0.11.3", ] @@ -6482,13 +6383,24 @@ 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.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator", + "phf_generator 0.11.3", "phf_shared 0.11.3", ] @@ -6502,17 +6414,40 @@ 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.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "phf_generator", + "phf_generator 0.11.3", "phf_shared 0.11.3", "proc-macro2", "quote", "syn 2.0.114", +] + +[[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.114", "unicase", ] @@ -6523,7 +6458,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", - "unicase", ] [[package]] @@ -6535,6 +6469,16 @@ dependencies = [ "siphasher", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", + "unicase", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -6584,21 +6528,6 @@ dependencies = [ "spki", ] -[[package]] -name = "pkcs5" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" -dependencies = [ - "aes", - "cbc", - "der", - "pbkdf2", - "scrypt", - "sha2", - "spki", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -6606,8 +6535,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", - "pkcs5", - "rand_core 0.6.4", "spki", ] @@ -6617,6 +6544,23 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "platform-nats" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-nats", + "bytes", + "futures", + "nkeys", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-test", + "tracing", +] + [[package]] name = "png" version = "0.18.0" @@ -6630,20 +6574,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.1.3", - "windows-sys 0.61.2", -] - [[package]] name = "portable-atomic" version = "1.13.0" @@ -6689,6 +6619,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "pretty" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156" +dependencies = [ + "arrayvec 0.5.2", + "typed-arena", + "unicode-width 0.2.2", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -6699,6 +6640,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -6746,16 +6696,16 @@ dependencies = [ "fnv", "lazy_static", "memchr", - "parking_lot 0.12.5", + "parking_lot", "protobuf", "thiserror 2.0.18", ] [[package]] name = "prost" -version = "0.13.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", "prost-derive", @@ -6763,16 +6713,15 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.13.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ "heck", "itertools 0.14.0", "log", "multimap", - "once_cell", - "petgraph 0.7.1", + "petgraph 0.8.3", "prettyplease", "prost", "prost-types", @@ -6783,9 +6732,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6796,9 +6745,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.13.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" dependencies = [ "prost", ] @@ -6883,38 +6832,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quick_cache" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb55a1aa7668676bb93926cd4e9cdfe60f03bb866553bcca9112554911b6d3dc" -dependencies = [ - "ahash 0.8.12", - "equivalent", - "hashbrown 0.14.5", - "parking_lot 0.12.5", -] - [[package]] name = "quick_cache" version = "0.6.18" @@ -6924,7 +6841,7 @@ dependencies = [ "ahash 0.8.12", "equivalent", "hashbrown 0.16.1", - "parking_lot 0.12.5", + "parking_lot", ] [[package]] @@ -6939,8 +6856,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.36", - "socket2 0.6.2", + "rustls", + "socket2", "thiserror 2.0.18", "tokio", "tracing", @@ -6960,7 +6877,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.36", + "rustls", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -6978,7 +6895,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2", "tracing", "windows-sys 0.60.2", ] @@ -7006,9 +6923,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "radix_trie" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +checksum = "3b4431027dcd37fc2a73ef740b5f233aa805897935b8bce0195e41bbf9a3289a" dependencies = [ "endian-type", "nibble_vec", @@ -7131,7 +7048,7 @@ dependencies = [ "aligned-vec", "arbitrary", "arg_enum_proc_macro", - "arrayvec", + "arrayvec 0.7.6", "av-scenechange", "av1-grain", "bitstream-io", @@ -7215,35 +7132,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbc4a4ea2a66a41a1152c4b3d86e8954dc087bdf33af35446e6e176db4e73c8c" -[[package]] -name = "recursive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0786a43debb760f491b1bc0269fe5e84155353c67482b9e60d0cfb596054b43e" -dependencies = [ - "recursive-proc-macro-impl", - "stacker", -] - -[[package]] -name = "recursive-proc-macro-impl" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" -dependencies = [ - "quote", - "syn 2.0.114", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -7253,17 +7141,6 @@ dependencies = [ "bitflags 2.10.0", ] -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - [[package]] name = "redox_users" version = "0.5.2" @@ -7318,18 +7195,18 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" - [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "rend" version = "0.4.2" @@ -7339,38 +7216,6 @@ dependencies = [ "bytecheck", ] -[[package]] -name = "reqsign" -version = "0.16.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43451dbf3590a7590684c25fb8d12ecdcc90ed3ac123433e500447c7d77ed701" -dependencies = [ - "anyhow", - "async-trait", - "base64 0.22.1", - "chrono", - "form_urlencoded", - "getrandom 0.2.17", - "hex", - "hmac", - "home", - "http 1.4.0", - "jsonwebtoken", - "log", - "once_cell", - "percent-encoding", - "quick-xml 0.37.5", - "rand 0.8.5", - "reqwest 0.12.28", - "rsa", - "rust-ini", - "serde", - "serde_json", - "sha1", - "sha2", - "tokio", -] - [[package]] name = "reqwest" version = "0.12.28" @@ -7382,12 +7227,12 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.4.13", - "http 1.4.0", - "http-body 1.0.1", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", + "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "js-sys", @@ -7398,8 +7243,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.36", - "rustls-native-certs", + "rustls", + "rustls-native-certs 0.8.3", "rustls-pki-types", "serde", "serde_json", @@ -7407,7 +7252,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.4", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -7417,7 +7262,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.5", ] [[package]] @@ -7431,27 +7275,28 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.4.13", - "http 1.4.0", - "http-body 1.0.1", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", + "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "mime", + "mime_guess", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.36", + "rustls", "rustls-pki-types", "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -7465,33 +7310,25 @@ dependencies = [ [[package]] name = "revision" -version = "0.10.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f53179a035f881adad8c4d58a2c599c6b4a8325b989c68d178d7a34d1b1e4c" -dependencies = [ - "revision-derive 0.10.0", -] - -[[package]] -name = "revision" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b8ee532f15b2f0811eb1a50adf10d036e14a6cdae8d99893e7f3b921cb227d" +checksum = "11c3c8ec8b2be254beb5f8acdd80cdd57b7b5d40988c2ec3d0b7cdb6f7c2829b" dependencies = [ + "bytes", "chrono", - "geo 0.28.0", + "geo 0.31.0", "regex", - "revision-derive 0.11.0", - "roaring", + "revision-derive", + "roaring 0.11.3", "rust_decimal", "uuid", ] [[package]] name = "revision-derive" -version = "0.10.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0ec466e5d8dca9965eb6871879677bef5590cf7525ad96cae14376efb75073" +checksum = "76be63634a8b1809e663bc0b975d78f6883c0fadbcce9c52e19b9e421f423357" dependencies = [ "proc-macro2", "quote", @@ -7499,14 +7336,13 @@ dependencies = [ ] [[package]] -name = "revision-derive" -version = "0.11.0" +name = "rfc6979" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3415e1bc838c36f9a0a2ac60c0fa0851c72297685e66592c44870d82834dfa2" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", + "hmac", + "subtle", ] [[package]] @@ -7525,7 +7361,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -7577,20 +7413,21 @@ dependencies = [ "serde", ] -[[package]] -name = "rmpv" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4e1d4b9b938a26d2996af33229f0ca0956c652c1375067f0b45291c1df8417" -dependencies = [ - "rmp", -] - [[package]] name = "roaring" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41589aba99537475bf697f2118357cad1c31590c5a1b9f6d9fc4ad6d07503661" +dependencies = [ + "bytemuck", + "byteorder", +] + +[[package]] +name = "roaring" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba9ce64a8f45d7fc86358410bb1a82e8c987504c0d4900e9141d69a9f26c885" dependencies = [ "bytemuck", "byteorder", @@ -7617,7 +7454,6 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core 0.6.4", - "sha2", "signature", "spki", "subtle", @@ -7686,13 +7522,32 @@ dependencies = [ ] [[package]] -name = "rust-ini" -version = "0.21.3" +name = "rstest" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" dependencies = [ "cfg-if", - "ordered-multimap", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.114", + "unicode-ident", ] [[package]] @@ -7711,7 +7566,7 @@ version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" dependencies = [ - "arrayvec", + "arrayvec 0.7.6", "borsh", "bytes", "num-traits", @@ -7728,13 +7583,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] -name = "rustc_lexer" -version = "0.1.0" +name = "rustc-literal-escaper" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86aae0c77166108c01305ee1a36a1e77289d7dc6ca0a3cd91ff4992de2d16a5" -dependencies = [ - "unicode-xid", -] +checksum = "8be87abb9e40db7466e0681dc8ecd9dcfd40360cb10b4c8fe24a7c4c3669b198" [[package]] name = "rustc_version" @@ -7771,18 +7623,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.23.36" @@ -7799,6 +7639,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe 0.1.6", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.3" @@ -7841,8 +7694,8 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.36", - "rustls-native-certs", + "rustls", + "rustls-native-certs 0.8.3", "rustls-platform-verifier-android", "rustls-webpki 0.103.9", "security-framework 3.5.1", @@ -7859,12 +7712,12 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ - "ring", - "untrusted", + "rustls-pki-types", + "untrusted 0.9.0", ] [[package]] @@ -7876,7 +7729,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -7977,22 +7830,26 @@ dependencies = [ "sha2", ] -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "seahash" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.7", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -8029,6 +7886,16 @@ dependencies = [ "libc", ] +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "semver" version = "1.0.27" @@ -8039,12 +7906,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "send_wrapper" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" - [[package]] name = "seq-macro" version = "0.3.6" @@ -8061,15 +7922,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-content" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3753ca04f350fa92d00b6146a3555e63c55388c9ef2e11e09bce2ff1c0b509c6" -dependencies = [ - "serde", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -8104,6 +7956,26 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_nanos" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93142f0367a4cc53ae0fead1bcda39e85beccfad3dcd717656cacab94b12985" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -8193,6 +8065,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -8217,6 +8099,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sif-itree" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142099cd6db3c4fab61e5133c62ff80b26674391e195860791fda0b1be3e5080" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -8227,6 +8115,18 @@ dependencies = [ "libc", ] +[[package]] +name = "signatory" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31" +dependencies = [ + "pkcs8", + "rand_core 0.6.4", + "signature", + "zeroize", +] + [[package]] name = "signature" version = "2.2.0" @@ -8291,35 +8191,23 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot 0.11.2", -] - [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smol_str" -version = "0.2.2" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +checksum = "0f7a918bd2a9951d18ee6e48f076843e8e73a9a5d22cf05bcd4b7a81bdd04e17" dependencies = [ - "serde", + "borsh", + "serde_core", ] [[package]] @@ -8349,16 +8237,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.2" @@ -8425,12 +8303,11 @@ dependencies = [ [[package]] name = "sqlparser" -version = "0.58.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec4b661c54b1e4b603b37873a18c59920e4c51ea8ea2cf527d925424dbd4437c" +checksum = "4591acadbcf52f0af60eafbb2c003232b2b4cd8de5f0e9437cb8b1b59046cc0f" dependencies = [ "log", - "recursive", "sqlparser_derive", ] @@ -8470,12 +8347,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "static_assertions_next" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" - [[package]] name = "std_prelude" version = "0.2.12" @@ -8490,22 +8361,32 @@ checksum = "e51f1e89f093f99e7432c491c382b88a6860a5adbe6bf02574bf0a08efff1978" [[package]] name = "storekey" -version = "0.5.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c42833834a5d23b344f71d87114e0cc9994766a5c42938f4b50e7b2aef85b2" +checksum = "bd9a94571bde7369ecaac47cec2e6844642d99166bd452fbd8def74b5b917b2f" dependencies = [ - "byteorder", - "memchr", - "serde", - "thiserror 1.0.69", + "bytes", + "storekey-derive", + "uuid", +] + +[[package]] +name = "storekey-derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6079d53242246522ec982de613c5c952cc7b1380ef2f8622fcdab9bfe73c0098" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", ] [[package]] name = "stratum-embeddings" version = "0.1.0" dependencies = [ - "approx 0.5.1", - "arrow", + "approx", + "arrow 56.2.0", "async-trait", "fastembed", "futures", @@ -8516,7 +8397,6 @@ dependencies = [ "reqwest 0.13.1", "serde", "serde_json", - "sled", "surrealdb", "tempfile", "thiserror 2.0.18", @@ -8527,6 +8407,23 @@ dependencies = [ "xxhash-rust", ] +[[package]] +name = "stratum-graph" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "serde", + "serde_json", + "surrealdb", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-test", + "tracing", + "uuid", +] + [[package]] name = "stratum-llm" version = "0.1.0" @@ -8551,6 +8448,55 @@ dependencies = [ "xxhash-rust", ] +[[package]] +name = "stratum-orchestrator" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "bytes", + "cedar-policy", + "chrono", + "dashmap", + "notify", + "platform-nats", + "regex", + "reqwest 0.13.1", + "serde", + "serde_json", + "stratum-graph", + "stratum-state", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-test", + "tokio-util", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "stratum-state" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "serde", + "serde_json", + "stratum-graph", + "surrealdb", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-test", + "tracing", + "uuid", +] + [[package]] name = "string_cache" version = "0.8.9" @@ -8558,7 +8504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", - "parking_lot 0.12.5", + "parking_lot", "phf_shared 0.11.3", "precomputed-hash", "serde", @@ -8570,7 +8516,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" dependencies = [ - "phf_generator", + "phf_generator 0.11.3", "phf_shared 0.11.3", "proc-macro2", "quote", @@ -8588,16 +8534,7 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros 0.26.4", -] - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros 0.27.2", + "strum_macros", ] [[package]] @@ -8613,18 +8550,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "subtle" version = "2.6.1" @@ -8633,156 +8558,266 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "surrealdb" -version = "2.6.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b7720b39ce2985efbfa10858b7397ffd95655a9bab6d9dfaa03622bbdc3bc2" +checksum = "2150ced737a7dd6b1ba97b9c3579282c9a18263f88a73700103de0e32f442666" dependencies = [ - "arrayvec", + "anyhow", "async-channel", - "bincode", + "boxcar", "chrono", - "dmp", "futures", - "geo 0.28.0", "getrandom 0.3.4", "indexmap 2.13.0", + "js-sys", "path-clean", - "pharos", - "reblessive", - "reqwest 0.12.28", - "revision 0.11.0", + "reqwest 0.13.1", "ring", - "rust_decimal", - "rustls 0.23.36", + "rustls", "rustls-pki-types", "semver", "serde", - "serde-content", "serde_json", "surrealdb-core", - "thiserror 1.0.69", + "surrealdb-types", "tokio", "tokio-tungstenite", + "tokio-tungstenite-wasm", "tokio-util", "tracing", - "trice", "url", "uuid", + "wasm-bindgen", "wasm-bindgen-futures", "wasmtimer", - "ws_stream_wasm", + "web-sys", ] [[package]] name = "surrealdb-core" -version = "2.6.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c48e42c81713be2f9b3dae64328999eafe8b8060dd584059445a908748b39787" +checksum = "225816a998dff5f898b72925494487877dd42fdef9985799433801e5ea8113c8" dependencies = [ "addr", "affinitypool", "ahash 0.8.12", "ammonia", - "any_ascii", + "anyhow", "argon2", "async-channel", - "async-executor", - "async-graphql", - "base64 0.21.7", + "async-stream", + "async-trait", + "base64 0.22.1", "bcrypt", - "bincode", "blake3", "bytes", - "castaway", - "cedar-policy", "chrono", "ciborium", - "dashmap 5.5.3", + "dashmap", "deunicode", "dmp", "ext-sort", + "fastnum", "fst", "futures", "fuzzy-matcher", - "geo 0.28.0", + "geo 0.32.0", "geo-types", "getrandom 0.3.4", - "hashbrown 0.14.5", + "headers", "hex", - "http 1.4.0", + "http", + "humantime", "ipnet", "jsonwebtoken", "lexicmp", - "linfa-linalg", "md-5", - "nanoid", - "ndarray 0.15.6", + "mime", + "ndarray 0.17.2", "ndarray-stats", "num-traits", "num_cpus", - "object_store", - "parking_lot 0.12.5", + "object_store 0.13.1", + "parking_lot", + "path-clean", "pbkdf2", - "pharos", - "phf 0.11.3", + "phf 0.13.1", "pin-project-lite", - "quick_cache 0.5.2", + "quick_cache", "radix_trie", "rand 0.8.5", "rayon", "reblessive", "regex", - "revision 0.11.0", + "revision", "ring", - "rmpv", - "roaring", + "roaring 0.11.3", "rust-stemmers", "rust_decimal", "scrypt", "semver", "serde", - "serde-content", "serde_json", "sha1", "sha2", - "snap", "storekey", "strsim", "subtle", + "surrealdb-protocol", + "surrealdb-rocksdb", + "surrealdb-types", "surrealkv", + "surrealmx", "sysinfo", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", + "tokio-util", "tracing", "trice", "ulid", "unicase", "url", "uuid", - "vart 0.8.1", + "vart", "wasm-bindgen-futures", "wasmtimer", - "ws_stream_wasm", +] + +[[package]] +name = "surrealdb-librocksdb-sys" +version = "0.17.3+10.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db194f1cf601bb6f2d0f4cbf0931bc3e5a602bac41ef2e9a87eccdfb28b7fed2" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "libc", + "libz-sys", + "lz4-sys", + "zstd-sys", +] + +[[package]] +name = "surrealdb-protocol" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb37698e0493bcfac3229ecb6ec6894a3ad705a3a2087b1562eeb881b3db19d4" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "chrono", + "flatbuffers", + "futures", + "geo 0.32.0", + "prost", + "prost-types", + "rust_decimal", + "semver", + "serde", + "serde_json", + "tonic", + "tonic-prost", + "uuid", +] + +[[package]] +name = "surrealdb-rocksdb" +version = "0.24.0-surreal.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057727f56d48825ddbe45e4e7401cda6e99d864fbc004e7474b4689a5e72c86d" +dependencies = [ + "libc", + "surrealdb-librocksdb-sys", +] + +[[package]] +name = "surrealdb-types" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72231b6b94c1dbd6e2453c50266c679eb621701cd01cb8f8fc32923d3ce0a217" +dependencies = [ + "anyhow", + "bytes", + "chrono", + "flatbuffers", + "geo 0.32.0", + "hex", + "http", + "papaya", + "rand 0.8.5", + "regex", + "rstest", + "rust_decimal", + "serde", + "serde_json", + "surrealdb-protocol", + "surrealdb-types-derive", + "ulid", + "uuid", +] + +[[package]] +name = "surrealdb-types-derive" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef7818b988bb8a8d424751ba5c0fe6aabf3e41dc26fbc8a922803c1615ae641b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", ] [[package]] name = "surrealkv" -version = "0.9.3" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a5041979bdff8599a1d5f6cb7365acb9a79664e2a84e5c4fddac2b3969f7d1" +checksum = "a6c1dd97851edc773c24afb282d6a17eb1bc798fafac53ea0d2951458854d472" dependencies = [ - "ahash 0.8.12", + "arc-swap", + "async-trait", + "byteorder", "bytes", "chrono", "crc32fast", - "double-ended-peekable", - "getrandom 0.2.17", - "lru", - "parking_lot 0.12.5", - "quick_cache 0.6.18", - "revision 0.10.0", - "vart 0.9.3", + "crossbeam-skiplist", + "fs2", + "getrandom 0.3.4", + "guardian", + "integer-encoding", + "log", + "lz4_flex 0.12.0", + "parking_lot", + "quick_cache", + "rand 0.9.2", + "scopeguard", + "sha2", + "snap", + "tokio", +] + +[[package]] +name = "surrealmx" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703077c9f8bdce4053dce39c5f5ef71b79b8fffe1e59dbd629b455340586814e" +dependencies = [ + "arc-swap", + "bincode", + "bytes", + "crossbeam-deque", + "crossbeam-queue", + "crossbeam-skiplist", + "lz4", + "papaya", + "parking_lot", + "serde", + "smallvec", + "thiserror 2.0.18", + "tracing", ] [[package]] @@ -8829,15 +8864,15 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.33.1" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" dependencies = [ - "core-foundation-sys", "libc", "memchr", "ntapi", - "rayon", + "objc2-core-foundation", + "objc2-io-kit", "windows", ] @@ -8893,7 +8928,7 @@ dependencies = [ "levenshtein_automata", "log", "lru", - "lz4_flex", + "lz4_flex 0.11.5", "measure_time", "memmap2", "once_cell", @@ -9022,12 +9057,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix 1.1.3", "windows-sys 0.61.2", @@ -9046,13 +9081,11 @@ dependencies = [ [[package]] name = "term" -version = "0.7.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "dirs-next", - "rustversion", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -9113,17 +9146,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "thrift" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e54bc85fc7faa8bc175c4bab5b92ba8d9a3ce893d0e9f42cc455c8ab16a9e09" -dependencies = [ - "byteorder", - "integer-encoding", - "ordered-float 2.10.1", -] - [[package]] name = "tiff" version = "0.10.3" @@ -9245,10 +9267,10 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot 0.12.5", + "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -9274,23 +9296,13 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.36", + "rustls", "tokio", ] @@ -9318,20 +9330,39 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.23.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", - "rustls 0.23.36", + "rustls", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tungstenite", "webpki-roots 0.26.11", ] +[[package]] +name = "tokio-tungstenite-wasm" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee909c02b8863f9bda87253127eb4da0e7e1342330b2583fbc4d1795c2f8" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "httparse", + "js-sys", + "thiserror 2.0.18", + "tokio", + "tokio-tungstenite", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -9342,10 +9373,32 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] +[[package]] +name = "tokio-websockets" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591660438b3038dd04d16c938271c79e7e06260ad2ea2885a4861bfb238605d" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-sink", + "http", + "httparse", + "rand 0.8.5", + "ring", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tokio-util", + "webpki-roots 0.26.11", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -9376,6 +9429,46 @@ dependencies = [ "winnow", ] +[[package]] +name = "tonic" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.3" @@ -9384,11 +9477,15 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", + "indexmap 2.13.0", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -9402,8 +9499,8 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "iri-string", "pin-project-lite", @@ -9412,6 +9509,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -9432,6 +9530,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -9505,22 +9604,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "tungstenite" -version = "0.23.0" +name = "tryhard" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +checksum = "9fe58ebd5edd976e0fe0f8a14d2a04b7c81ef153ea9a54eebc42e67c2c23b4e5" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ - "byteorder", "bytes", "data-encoding", - "http 1.4.0", + "http", "httparse", "log", - "rand 0.8.5", - "rustls 0.23.36", + "rand 0.9.2", + "rustls", "rustls-pki-types", "sha1", - "thiserror 1.0.69", + "thiserror 2.0.18", "url", "utf-8", ] @@ -9534,18 +9642,18 @@ dependencies = [ "rand 0.9.2", ] +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - [[package]] name = "ulid" version = "1.2.1" @@ -9639,12 +9747,24 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "ureq" version = "2.12.1" @@ -9656,7 +9776,7 @@ dependencies = [ "log", "native-tls", "once_cell", - "rustls 0.23.36", + "rustls", "rustls-pki-types", "serde", "serde_json", @@ -9690,7 +9810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" dependencies = [ "base64 0.22.1", - "http 1.4.0", + "http", "httparse", "log", ] @@ -9733,11 +9853,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.1", "js-sys", "serde_core", "wasm-bindgen", @@ -9760,12 +9880,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "vart" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87782b74f898179396e93c0efabb38de0d58d50bbd47eae00c71b3a1144dbbae" - [[package]] name = "vart" version = "0.9.3" @@ -9785,10 +9899,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "vsimd" -version = "0.8.0" +name = "virtue" +version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" [[package]] name = "walkdir" @@ -9824,6 +9938,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -9883,6 +10006,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -9897,14 +10042,26 @@ dependencies = [ ] [[package]] -name = "wasmtimer" -version = "0.2.1" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7ed9d8b15c7fb594d72bfb4b5a276f3d2029333cd93a932f376f5937f6f80ee" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "wasmtimer" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" dependencies = [ "futures", "js-sys", - "parking_lot 0.12.5", + "parking_lot", "pin-utils", "wasm-bindgen", ] @@ -10018,24 +10175,37 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.57.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", ] [[package]] name = "windows-core" -version = "0.57.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -10044,22 +10214,22 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link", + "windows-implement", + "windows-interface", + "windows-link 0.2.1", "windows-result 0.4.1", - "windows-strings", + "windows-strings 0.5.1", ] [[package]] -name = "windows-implement" -version = "0.57.0" +name = "windows-future" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] @@ -10073,17 +10243,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -10095,30 +10254,46 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-registry" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows-result 0.4.1", - "windows-strings", + "windows-strings 0.5.1", ] [[package]] name = "windows-result" -version = "0.1.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-targets 0.52.6", + "windows-link 0.1.3", ] [[package]] @@ -10127,7 +10302,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -10136,7 +10320,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -10181,7 +10365,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -10221,7 +10405,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -10232,6 +10416,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -10390,6 +10583,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.114", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "wkb" @@ -10422,25 +10697,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" -[[package]] -name = "ws_stream_wasm" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" -dependencies = [ - "async_io_stream", - "futures", - "js-sys", - "log", - "pharos", - "rustc_version", - "send_wrapper", - "thiserror 2.0.18", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "wyz" version = "0.5.1" @@ -10450,27 +10706,12 @@ dependencies = [ "tap", ] -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - [[package]] name = "xxhash-rust" version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - [[package]] name = "y4m" version = "0.8.0" @@ -10580,12 +10821,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "zlib-rs" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" - [[package]] name = "zmij" version = "1.0.16" diff --git a/Cargo.toml b/Cargo.toml index 70b0e62..746c14b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,14 +23,13 @@ humantime-serde = "1.1" # Caching moka = { version = "0.12", features = ["future"] } -sled = "0.34" # Embeddings -fastembed = "5.8" +fastembed = "5.11" # Vector storage -lancedb = "0.23" -surrealdb = { version = "2.6", features = ["kv-mem"] } +lancedb = "0.26" +surrealdb = { version = "3", features = ["kv-mem"] } # LOCKED: Arrow 56.x required for LanceDB 0.23 compatibility # LanceDB 0.23 uses Arrow 56.2.0 internally - Arrow 57 breaks API compatibility # DO NOT upgrade to Arrow 57 until LanceDB supports it @@ -51,10 +50,23 @@ prometheus = "0.14" xxhash-rust = { version = "0.8", features = ["xxh3"] } dirs = "6.0" chrono = "0.4" -uuid = "1.20" +uuid = { version = "1.21", features = ["v4"] } which = "8.0" +bytes = "1.11" + +# Orchestration +async-nats = "0.46" +dashmap = "6.1" +notify = { version = "8.2", default-features = false, features = ["macos_fsevent"] } +cedar-policy = "4.9" +nkeys = "0.4" +regex = "1.12" +tokio-util = { version = "0.7", features = ["rt"] } +tokio-stream = "0.1" +axum = { version = "0.8", features = ["json"] } +tower-http = { version = "0.6", features = ["trace"] } # Testing tokio-test = "0.4" approx = "0.5" -tempfile = "3.24" +tempfile = "3.25" diff --git a/action-nodes/build-crate.ncl b/action-nodes/build-crate.ncl new file mode 100644 index 0000000..42a4f2f --- /dev/null +++ b/action-nodes/build-crate.ncl @@ -0,0 +1,15 @@ +let base = import "../nickel/stratum-base/stratum-base.ncl" in +{ + id = "build-crate", + handler = "scripts/nu/build.nu", + input_schemas = { + "linted-code" = "schemas/capabilities/linted-code.ncl", + "formatted-code" = "schemas/capabilities/formatted-code.ncl", + }, + output_schemas = { "built-artifact" = "schemas/capabilities/built-artifact.ncl" }, + compensate = "scripts/nu/build-rollback.nu", + retry = { max = 3, backoff_secs = 10, strategy = 'exponential }, + timeout_secs = 600, + atomic = true, + triggers = [], +} | base.NodeDefinition diff --git a/action-nodes/fmt-crate.ncl b/action-nodes/fmt-crate.ncl new file mode 100644 index 0000000..7cd5329 --- /dev/null +++ b/action-nodes/fmt-crate.ncl @@ -0,0 +1,12 @@ +let base = import "../nickel/stratum-base/stratum-base.ncl" in +{ + id = "fmt-crate", + handler = "scripts/nu/fmt.nu", + input_schemas = {}, + output_schemas = { "formatted-code" = "schemas/capabilities/formatted-code.ncl" }, + compensate = null, + retry = { max = 1, backoff_secs = 5, strategy = 'fixed }, + timeout_secs = 60, + atomic = true, + triggers = ["dev.crate.*.modified"], +} | base.NodeDefinition diff --git a/action-nodes/install-crate.ncl b/action-nodes/install-crate.ncl new file mode 100644 index 0000000..47689b6 --- /dev/null +++ b/action-nodes/install-crate.ncl @@ -0,0 +1,12 @@ +let base = import "../nickel/stratum-base/stratum-base.ncl" in +{ + id = "install-crate", + handler = "scripts/nu/install.nu", + input_schemas = { "built-artifact" = "schemas/capabilities/built-artifact.ncl" }, + output_schemas = { "installed" = "schemas/capabilities/installed.ncl" }, + compensate = "scripts/nu/install-rollback.nu", + retry = { max = 2, backoff_secs = 5, strategy = 'linear }, + timeout_secs = 180, + atomic = true, + triggers = [], +} | base.NodeDefinition diff --git a/action-nodes/lint-crate.ncl b/action-nodes/lint-crate.ncl new file mode 100644 index 0000000..638b6f6 --- /dev/null +++ b/action-nodes/lint-crate.ncl @@ -0,0 +1,12 @@ +let base = import "../nickel/stratum-base/stratum-base.ncl" in +{ + id = "lint-crate", + handler = "scripts/nu/lint.nu", + input_schemas = {}, + output_schemas = { "linted-code" = "schemas/capabilities/linted-code.ncl" }, + compensate = null, + retry = { max = 2, backoff_secs = 5, strategy = 'fixed }, + timeout_secs = 120, + atomic = true, + triggers = ["dev.crate.*.modified"], +} | base.NodeDefinition diff --git a/action-nodes/notify-result.ncl b/action-nodes/notify-result.ncl new file mode 100644 index 0000000..6eb78ce --- /dev/null +++ b/action-nodes/notify-result.ncl @@ -0,0 +1,12 @@ +let base = import "../nickel/stratum-base/stratum-base.ncl" in +{ + id = "notify-result", + handler = "scripts/nu/notify.nu", + input_schemas = { "installed" = "schemas/capabilities/installed.ncl" }, + output_schemas = {}, + compensate = null, + retry = { max = 3, backoff_secs = 2, strategy = 'exponential }, + timeout_secs = 30, + atomic = false, + triggers = [], +} | base.NodeDefinition diff --git a/assets/diagrams/orchestrator/arch-stratum-orchestrator.svg b/assets/diagrams/orchestrator/arch-stratum-orchestrator.svg new file mode 100644 index 0000000..0685f35 --- /dev/null +++ b/assets/diagrams/orchestrator/arch-stratum-orchestrator.svg @@ -0,0 +1,477 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +STRATUM ORCHESTRATOR +Event-Driven · Graph-Guided · Atomic Execution · Stateless · OCI-Native + + +EVENT SOURCES + + + + +provisioning +emits dev.crate.> +crate-modified · deploy + + + + +kogral +emits dev.knowledge.> +node-updated · indexed + + + + +syntaxis +emits dev.project.> +phase · task-completed + + + + +stratumiops +emits dev.model.> +llm-call · embed-request + + + + +typedialog +emits dev.form.> +submitted · validated + + ++ more ... + + + + + + + + + + + + + + + + + + + + +NATS +JetStream +dev.> + + + + + + + + + + + + + + + + + +NKey verify +ed25519 JWT + + + + + + +STRATUM ORCHESTRATOR +Agnostic · Stateless · Graph-Guided + + + + +◈ ActionGraph +in-memory · Nickel nodes +topo-sort · cycle-detect + + + +⬡ PipelineCtx +DB-first · typed caps +schema-validated + + + +▶ StageRunner +JoinSet · parallel stages +CancellationToken +retry + backoff on failure +saga compensate.nu + + + +⚙ RuleEngine + +Cedar + +◈ Cedar +permit · forbid · conditions +per-node authz policies + +◈ NKey +ed25519 asymmetric keys +JWT per-process · verify + +↺ Saga rollback on failure · compensate.nu in reverse + + + + + + + + + + + +SurrealDB +Pipeline state · Step results +orchestrator_state ns +crash recovery + + + + + +SecretumVault +Credentials · TTL leases +vault:/secret/... +never in NATS payload + + + + + +Zot OCI Registry +Node defs · Nickel libs +oci://registry/nodes/ +content-addressed · signed + + +OPTIONAL + + + + + + + + +Kogral +Knowledge graph +node-updated triggers + +optional + + + +Syntaxis +Project orchestration +phase-transition events + +optional + + + +TypeDialog +Service config UI +startup config NCL only + + + + + + + + + + +Git repo +Git events → NATS +webhook → dev.crate.> +push · tag · pr + + + + + + + + + + +Nu Executor +Atomic steps · Pure functions +scripts/nu/*.nu +stdout=output · exit-code=status + + + + +AI Agent +stratum-llm · NATS protocol +dev.agent.*.requested/responded +oneshot correlation · timeout + + + + + +Nickel Base Library +OCI-published · content-addressed · build-verified · typecheck-gated +orchestrator-types.ncl · capability-schemas.ncl · defaults.ncl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + StratumIOps + + + + + + + +LEGEND + +event flow (NATS) + +auth / credentials + +execution + +state (DB) + +OCI / registry + +optional + + +stratumiops · v0.1 + diff --git a/assets/diagrams/orchestrator/flow-stratum-build-pipeline.svg b/assets/diagrams/orchestrator/flow-stratum-build-pipeline.svg new file mode 100644 index 0000000..309b640 --- /dev/null +++ b/assets/diagrams/orchestrator/flow-stratum-build-pipeline.svg @@ -0,0 +1,450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +STRATUM ORCHESTRATOR · BUILD PIPELINE FLOW +event: dev.crate.provisioning-cli.modified · trigger → ActionGraph → stages → state → emit + + + + + + + + +Developer · Forgejo +git push / webhook + + + + +NATS JetStream +Event Bus + + + + +Orchestrator +Planning + + + + +Auth · NKeys ++ Cedar + + + + +Stage 0 +‖ Parallel + + + + +Stage 1 +→ Sequential + + + + +Stage 2 +→ Sequential + + + + +SurrealDB +State + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + git push + code change + commit: abc123f + + + + + + 2 + + dev.crate. + provisioning-cli + .modified + + {"crate":"prvng-cli", + "sha":"abc123f"} + + + + + + 3 + + pull_batch + (10) + JetStream consumer + + + + + + 4 + + NKey + verify + publisher auth + + + + + + 5 + + ActionGraph + query + topo-sort + + + [lint-crate, fmt-crate] + → build-crate + → install + + + Stages + + + + + + 6 + + Cedar + authorize + permit: trigger + node:build-crate + + + + + +‖ parallel · JoinSet + + + 7 + + + + + + lint.nu + (clippy) + + fmt.nu + (cargo fmt) + + + +join_all → PipelineContext + + + + + 8 + + deposit: + linted-code + formatted-code + + + + + + 9 + + build.nu + (cargo build) + + + 🔐 Vault cred + TTL=step timeout + deposit: built-artifact + + + + + + 10 + + install.nu + extracts: built-artifact + deposit: installed + + + + + + 11 + + pipeline_run + status: success + all steps persisted + + + + + + 12 + + dev.crate. + provisioning-cli + .built + duration_ms, status + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +rollback path: compensate.nu in reverse order + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + StratumIOps + + + + + + +Legend: + + +NATS events + + +Orchestrator + + +Auth (NKeys + Cedar) + + +Execution (Stage 0) + + +State (SurrealDB) + + +Rollback path + +StratumIOps · stratum-orchestrator +stratum-orchestrator · build pipeline + diff --git a/assets/diagrams/orchestrator/w-arch-stratum-orchestrator.svg b/assets/diagrams/orchestrator/w-arch-stratum-orchestrator.svg new file mode 100644 index 0000000..59e2814 --- /dev/null +++ b/assets/diagrams/orchestrator/w-arch-stratum-orchestrator.svg @@ -0,0 +1,469 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +STRATUM ORCHESTRATOR +Event-Driven · Graph-Guided · Atomic Execution · Stateless · OCI-Native + + +EVENT SOURCES + + + + +provisioning +emits dev.crate.> +crate-modified · deploy + + + + +kogral +emits dev.knowledge.> +node-updated · indexed + + + + +syntaxis +emits dev.project.> +phase · task-completed + + + + +stratumiops +emits dev.model.> +llm-call · embed-request + + + + +typedialog +emits dev.form.> +submitted · validated + + ++ more ... + + + + + + + + + + + + + + + + + + +NATS +JetStream +dev.> + + + + + + + + + + + + + + + + + +NKey verify +ed25519 JWT + + + + + + +STRATUM ORCHESTRATOR +Agnostic · Stateless · Graph-Guided + + + + +◈ ActionGraph +in-memory · Nickel nodes +topo-sort · cycle-detect + + + +⬡ PipelineCtx +DB-first · typed caps +schema-validated + + + +▶ StageRunner +JoinSet · parallel stages +CancellationToken +retry + backoff on failure +saga compensate.nu + + + +⚙ RuleEngine + +Cedar + +◈ Cedar +permit · forbid · conditions +per-node authz policies + +◈ NKey +ed25519 asymmetric keys +JWT per-process · verify + +↺ Saga rollback on failure · compensate.nu in reverse + + + + + + + + + + +SurrealDB +Pipeline state · Step results +orchestrator_state ns +crash recovery + + + + + +SecretumVault +Credentials · TTL leases +vault:/secret/... +never in NATS payload + + + + + +Zot OCI Registry +Node defs · Nickel libs +oci://registry/nodes/ +content-addressed · signed + + +OPTIONAL + + + + + + + +Kogral +Knowledge graph +node-updated triggers + +optional + + + +Syntaxis +Project orchestration +phase-transition events + +optional + + + +TypeDialog +Service config UI +startup config NCL only + + + + + + + + +Git repo +Git events → NATS +webhook → dev.crate.> +push · tag · pr + + + + + + + + +Nu Executor +Atomic steps · Pure functions +scripts/nu/*.nu +stdout=output · exit-code=status + + + + +AI Agent +stratum-llm · NATS protocol +dev.agent.*.requested/responded +oneshot correlation · timeout + + + + + +Nickel Base Library +OCI-published · content-addressed · build-verified · typecheck-gated +orchestrator-types.ncl · capability-schemas.ncl · defaults.ncl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + StratumIOps + + + + + + + +LEGEND + +event flow (NATS) + +auth / credentials + +execution + +state (DB) + +OCI / registry + +optional + + +stratumiops · v0.1 + diff --git a/assets/diagrams/orchestrator/w-flow-stratum-build-pipeline.svg b/assets/diagrams/orchestrator/w-flow-stratum-build-pipeline.svg new file mode 100644 index 0000000..ea9e4cb --- /dev/null +++ b/assets/diagrams/orchestrator/w-flow-stratum-build-pipeline.svg @@ -0,0 +1,450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +STRATUM ORCHESTRATOR · BUILD PIPELINE FLOW +event: dev.crate.provisioning-cli.modified · trigger → ActionGraph → stages → state → emit + + + + + + + + +Developer · Forgejo +git push / webhook + + + + +NATS JetStream +Event Bus + + + + +Orchestrator +Planning + + + + +Auth · NKeys ++ Cedar + + + + +Stage 0 +‖ Parallel + + + + +Stage 1 +→ Sequential + + + + +Stage 2 +→ Sequential + + + + +SurrealDB +State + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + git push + code change + commit: abc123f + + + + + + 2 + + dev.crate. + provisioning-cli + .modified + + {"crate":"prvng-cli", + "sha":"abc123f"} + + + + + + 3 + + pull_batch + (10) + JetStream consumer + + + + + + 4 + + NKey + verify + publisher auth + + + + + + 5 + + ActionGraph + query + topo-sort + + + [lint-crate, fmt-crate] + → build-crate + → install + + + Stages + + + + + + 6 + + Cedar + authorize + permit: trigger + node:build-crate + + + + + +‖ parallel · JoinSet + + + 7 + + + + + + lint.nu + (clippy) + + fmt.nu + (cargo fmt) + + + +join_all → PipelineContext + + + + + 8 + + deposit: + linted-code + formatted-code + + + + + + 9 + + build.nu + (cargo build) + + + 🔐 Vault cred + TTL=step timeout + deposit: built-artifact + + + + + + 10 + + install.nu + extracts: built-artifact + deposit: installed + + + + + + 11 + + pipeline_run + status: success + all steps persisted + + + + + + 12 + + dev.crate. + provisioning-cli + .built + duration_ms, status + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +rollback path: compensate.nu in reverse order + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + StratumIOps + + + + + + +Legend: + + +NATS events + + +Orchestrator + + +Auth (NKeys + Cedar) + + +Execution (Stage 0) + + +State (SurrealDB) + + +Rollback path + +StratumIOps · stratum-orchestrator +stratum-orchestrator · build pipeline + diff --git a/assets/orchestrator/arch-stratum-orchestrator.svg b/assets/orchestrator/arch-stratum-orchestrator.svg new file mode 100644 index 0000000..0685f35 --- /dev/null +++ b/assets/orchestrator/arch-stratum-orchestrator.svg @@ -0,0 +1,477 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +STRATUM ORCHESTRATOR +Event-Driven · Graph-Guided · Atomic Execution · Stateless · OCI-Native + + +EVENT SOURCES + + + + +provisioning +emits dev.crate.> +crate-modified · deploy + + + + +kogral +emits dev.knowledge.> +node-updated · indexed + + + + +syntaxis +emits dev.project.> +phase · task-completed + + + + +stratumiops +emits dev.model.> +llm-call · embed-request + + + + +typedialog +emits dev.form.> +submitted · validated + + ++ more ... + + + + + + + + + + + + + + + + + + + + +NATS +JetStream +dev.> + + + + + + + + + + + + + + + + + +NKey verify +ed25519 JWT + + + + + + +STRATUM ORCHESTRATOR +Agnostic · Stateless · Graph-Guided + + + + +◈ ActionGraph +in-memory · Nickel nodes +topo-sort · cycle-detect + + + +⬡ PipelineCtx +DB-first · typed caps +schema-validated + + + +▶ StageRunner +JoinSet · parallel stages +CancellationToken +retry + backoff on failure +saga compensate.nu + + + +⚙ RuleEngine + +Cedar + +◈ Cedar +permit · forbid · conditions +per-node authz policies + +◈ NKey +ed25519 asymmetric keys +JWT per-process · verify + +↺ Saga rollback on failure · compensate.nu in reverse + + + + + + + + + + + +SurrealDB +Pipeline state · Step results +orchestrator_state ns +crash recovery + + + + + +SecretumVault +Credentials · TTL leases +vault:/secret/... +never in NATS payload + + + + + +Zot OCI Registry +Node defs · Nickel libs +oci://registry/nodes/ +content-addressed · signed + + +OPTIONAL + + + + + + + + +Kogral +Knowledge graph +node-updated triggers + +optional + + + +Syntaxis +Project orchestration +phase-transition events + +optional + + + +TypeDialog +Service config UI +startup config NCL only + + + + + + + + + + +Git repo +Git events → NATS +webhook → dev.crate.> +push · tag · pr + + + + + + + + + + +Nu Executor +Atomic steps · Pure functions +scripts/nu/*.nu +stdout=output · exit-code=status + + + + +AI Agent +stratum-llm · NATS protocol +dev.agent.*.requested/responded +oneshot correlation · timeout + + + + + +Nickel Base Library +OCI-published · content-addressed · build-verified · typecheck-gated +orchestrator-types.ncl · capability-schemas.ncl · defaults.ncl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + StratumIOps + + + + + + + +LEGEND + +event flow (NATS) + +auth / credentials + +execution + +state (DB) + +OCI / registry + +optional + + +stratumiops · v0.1 + diff --git a/assets/orchestrator/flow-stratum-build-pipeline.svg b/assets/orchestrator/flow-stratum-build-pipeline.svg new file mode 100644 index 0000000..309b640 --- /dev/null +++ b/assets/orchestrator/flow-stratum-build-pipeline.svg @@ -0,0 +1,450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +STRATUM ORCHESTRATOR · BUILD PIPELINE FLOW +event: dev.crate.provisioning-cli.modified · trigger → ActionGraph → stages → state → emit + + + + + + + + +Developer · Forgejo +git push / webhook + + + + +NATS JetStream +Event Bus + + + + +Orchestrator +Planning + + + + +Auth · NKeys ++ Cedar + + + + +Stage 0 +‖ Parallel + + + + +Stage 1 +→ Sequential + + + + +Stage 2 +→ Sequential + + + + +SurrealDB +State + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + git push + code change + commit: abc123f + + + + + + 2 + + dev.crate. + provisioning-cli + .modified + + {"crate":"prvng-cli", + "sha":"abc123f"} + + + + + + 3 + + pull_batch + (10) + JetStream consumer + + + + + + 4 + + NKey + verify + publisher auth + + + + + + 5 + + ActionGraph + query + topo-sort + + + [lint-crate, fmt-crate] + → build-crate + → install + + + Stages + + + + + + 6 + + Cedar + authorize + permit: trigger + node:build-crate + + + + + +‖ parallel · JoinSet + + + 7 + + + + + + lint.nu + (clippy) + + fmt.nu + (cargo fmt) + + + +join_all → PipelineContext + + + + + 8 + + deposit: + linted-code + formatted-code + + + + + + 9 + + build.nu + (cargo build) + + + 🔐 Vault cred + TTL=step timeout + deposit: built-artifact + + + + + + 10 + + install.nu + extracts: built-artifact + deposit: installed + + + + + + 11 + + pipeline_run + status: success + all steps persisted + + + + + + 12 + + dev.crate. + provisioning-cli + .built + duration_ms, status + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +rollback path: compensate.nu in reverse order + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + StratumIOps + + + + + + +Legend: + + +NATS events + + +Orchestrator + + +Auth (NKeys + Cedar) + + +Execution (Stage 0) + + +State (SurrealDB) + + +Rollback path + +StratumIOps · stratum-orchestrator +stratum-orchestrator · build pipeline + diff --git a/assets/orchestrator/minify.sh b/assets/orchestrator/minify.sh new file mode 100755 index 0000000..b789b76 --- /dev/null +++ b/assets/orchestrator/minify.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# Minify HTML files from src/ to production versions +# Usage: ./minify.sh +# Processes: stratumiops-arch, stratumiops-pipeline, stratumiops-orchestrator + +set -e + +SCRIPT_DIR="$(dirname "$0")" +FILES=("stratumiops-arch" "stratumiops-pipeline" "stratumiops-orchestrator") + +minify_file() { + local basename=$1 + local src_file="${SCRIPT_DIR}/src/${basename}.html" + local out_file="${SCRIPT_DIR}/${basename}.html" + local temp_file="${out_file}.tmp" + + if [ ! -f "$src_file" ]; then + echo "⚠️ Skipping $basename: source file not found: $src_file" + return 0 + fi + + echo "🔨 Minifying $basename.html..." + echo " Input: $src_file" + echo " Output: $out_file" + + perl -e " +use strict; +use warnings; + +open(my \$fh, '<', '$src_file') or die \$!; +my \$content = do { local \$/; <\$fh> }; +close(\$fh); + +# Remove HTML comments +\$content =~ s///gs; + +# Compress CSS (remove spaces and comments) +\$content =~ s/(]*>)(.*?)(<\/style>)/ + my \$before = \$1; + my \$style = \$2; + my \$after = \$3; + \$style =~ s{\/\*.*?\*\/}{}gs; + \$style =~ s{\s+}{ }gs; + \$style =~ s{\s*([{}:;,>+~])\s*}{\$1}gs; + \$before . \$style . \$after; +/gies; + +# Compress JavaScript (remove comments and extra spaces) +\$content =~ s/(]*>)(.*?)(<\/script>)/ + my \$before = \$1; + my \$script = \$2; + my \$after = \$3; + \$script =~ s{\/\/.*\$}{}gm; + \$script =~ s{\s+}{ }gs; + \$script =~ s{\s*([{}();,])\s*}{\$1}gs; + \$before . \$script . \$after; +/gies; + +# Remove whitespace between tags +\$content =~ s/>\s+', '$temp_file') or die \$!; +print \$out \$content; +close(\$out); +" || { + echo "❌ Minification failed for $basename" + rm -f "$temp_file" + return 1 + } + + mv "$temp_file" "$out_file" + + # Show statistics + original=$(wc -c < "$src_file") + minified=$(wc -c < "$out_file") + saved=$((original - minified)) + percent=$((saved * 100 / original)) + + echo " ✅ $basename.html minified" + printf " Original: %6d bytes | Minified: %6d bytes | Saved: %d%% (%d bytes)\n" "$original" "$minified" "$percent" "$saved" + echo "" +} + +echo "🔨 Minifying HTML files..." +echo "" + +for file in "${FILES[@]}"; do + minify_file "$file" || exit 1 +done + +echo "✅ All HTML files minified and ready for production" diff --git a/assets/orchestrator/src/stratumiops-arch.html b/assets/orchestrator/src/stratumiops-arch.html new file mode 100644 index 0000000..4e2bd3e --- /dev/null +++ b/assets/orchestrator/src/stratumiops-arch.html @@ -0,0 +1,185 @@ + + + + + + StratumIOps — Orchestrator Architecture + + + + + +
+ Stratum Orchestrator Architecture — Dark + +
+ + + + diff --git a/assets/orchestrator/src/stratumiops-orchestrator.html b/assets/orchestrator/src/stratumiops-orchestrator.html new file mode 100644 index 0000000..bd5fb6b --- /dev/null +++ b/assets/orchestrator/src/stratumiops-orchestrator.html @@ -0,0 +1,1423 @@ + + + + + + + Stratum Orchestrator + + + + + +
+ + + +
+
+ +
+ StratumIOps +
+

+ Graph-Driven Workflow Orchestration +

+

+ Stratum Orchestrator —
Stateless, Agnostic, Auditable +

+

+ Cross-project event-driven pipelines + + declared in Nickel, executed by Nushell, coordinated via NATS + JetStream, and persisted in SurrealDB. The orchestrator binary never + changes when workflows do. +

+
+ + +
+

+ 10 Fundamental Design Characteristics +

+
+
+
01
+

+ Graph-Driven — No Routing Tables +

+

+ NATS subjects are matched against an ActionGraph built from Nickel + node definitions. Each ActionNode declares trigger, input_schemas, + output_schemas, and compensate. Adding a workflow means adding + .ncl files — zero binary changes. +

+
+ +
+
02
+

+ Stateless Orchestrator — DB-First +

+

+ Every PipelineContext write goes to SurrealDB first, then to an + in-memory DashMap cache. On crash, the instance reconstructs from + DB. Enables horizontal scaling and crash-free pipeline resumption + from last persisted capability. +

+
+ +
+
03
+

+ Nickel as Single Source of Truth +

+

+ Action nodes, capability schemas, and startup config are all + defined in Nickel. No DB copy of node definitions exists at + runtime. The ActionGraph is built in-memory at startup via nickel + export, then kept live via a notify file watcher for hot-reload. +

+
+ +
+
04
+

+ Capability Model — DI at Execution Level +

+

+ Nodes do not depend on each other directly — they declare + capabilities they produce and consume. The graph engine resolves + dependencies. build-crate does not know about lint-crate; it only + needs linted-code. Nodes can be swapped or parallelized without + changing consumers. +

+
+ +
+
05
+

+ Three Independent Auth Planes +

+

+ Publisher auth via NATS NKeys (ed25519) controls who can emit + events. Workflow authz via Cedar policies controls which pipelines + a principal can trigger. Execution credentials via SecretumVault + are scoped per-node with TTL equal to the node timeout — revoked + on failure. +

+
+ +
+
06
+

+ Saga Atomicity — Compensation Not Transactions +

+

+ Pipelines execute forward through stages. If a stage fails, the + orchestrator runs compensate.nu scripts in reverse order through + all previously successful stages. Compensation is best-effort; + failures are logged and auditable in SurrealDB — the pipeline + still reaches Compensated status. +

+
+ +
+
07
+

+ Parallel Stages — JoinSet + CancellationToken +

+

+ Within each stage, nodes with no capability dependencies execute + in parallel via tokio::task::JoinSet. Fail-fast is implemented via + CancellationToken: the first node failure cancels the token, + aborting all sibling tasks in that stage immediately. +

+
+ +
+
08
+

+ OCI for Everything — Content-Addressed +

+

+ Node definitions and the Nickel base library are published as OCI + artifacts to a Zot registry. The ncl-import-resolver binary pulls + each OCI layer at startup, verifies digest against annotated + sha256, then exposes a local path for Nickel imports. Prevents + loading unverified definitions. +

+
+ +
+
09
+

+ Nushell as Execution Unit — Agnostic +

+

+ Each node's handler is a Nushell script. The executor spawns nu + --no-config-file script.nu, passes PipelineContext inputs as JSON + on stdin, reads output JSON from stdout. The orchestrator has no + knowledge of what the script does — hot-replaceable without + recompiling. +

+
+ +
+
10
+

+ TypeDialog — Startup Config Only +

+

+ TypeDialog is used exclusively for orchestrator startup + configuration: SurrealDB URL, NATS URL, Zot URL, Vault URL, log + level, feature flags. Not for workflow definitions or node + configurations — those live in Nickel. Bounded scope, single + responsibility. +

+
+
+
+ + +
+

+ Key Architectural Decisions +

+
+
+
🕸️
+

+ Capability DAG — not a rule router +

+

+ Topological sort of the capability graph produces a staged + execution plan. Each stage is a set of nodes with no + inter-dependency. The orchestrator evaluates the graph; it does + not hardcode any workflow logic. +

+
+ lint-crate → produces: linted-code fmt-crate → produces: + formatted-code build-crate → consumes: linted-code, formatted-code + → produces: built-artifact +
+
+ +
+
🔐
+

+ Three-Plane Auth — non-substitutable +

+

+ NKeys authenticate the publisher on the transport. Cedar + authorizes the workflow at the orchestrator boundary. Vault scopes + execution credentials per-node with TTL = node timeout, injected + as env vars and revoked on failure — never in NATS messages or + logs. +

+
+ Transport: NATS NKeys (ed25519) Authz: Cedar policies Execution: + Vault lease, TTL-scoped +
+
+ +
+
🔄
+

+ Saga Compensation — auditable rollback +

+

+ Compensation scripts run in reverse stage order through all + previously succeeded stages. The full trace — including + compensation failures — is recorded in SurrealDB. 2PC is not + feasible across Nushell subprocesses; Saga is the only realistic + atomicity model here. +

+
+ Stage 0: lint ✓ fmt ✓ → executed Stage 1: build ✗ → compensate + Stage 0 ←: undo fmt, undo lint +
+
+ +
+
+

+ DB-First State — crash recovery is free +

+

+ PipelineContext is written to SurrealDB before updating the + in-memory DashMap. On restart, the cache is reconstructed from the + DB. Multiple instances share one SurrealDB — no split-brain, no + coordinator needed. Horizontal scaling is structural, not bolted + on. +

+
+ +
+
📦
+

+ OCI Distribution — verified imports +

+

+ nickel typecheck → gitleaks detect → nickel export → sha256sum → + oras push with content-hash annotations. The ncl-import-resolver + verifies each layer's digest against the annotated hash before + exposing it to the Nickel import path — tampered definitions + cannot load. +

+
+ +
+
🐚
+

+ Nushell Executor — domain-agnostic subprocess +

+

+ Each node spawns nu --no-config-file script.nu. PipelineContext + inputs arrive as JSON on stdin; outputs return as JSON on stdout. + Each node gets its own process with scoped Vault credentials. + Scripts can be tested independently: echo '{}' | nu script.nu. +

+
+
+
+ + +
+

+ Architecture & Pipeline Diagrams +

+
+
+
+ + Orchestrator Architecture — Dark + Orchestrator Architecture — Light + +

+ Orchestrator Architecture +

+
+
+ + Build Pipeline Flow — Dark + Build Pipeline Flow — Light + +

+ Build Pipeline Flow +

+
+
+ +
+
+ + +
+

+ Crate Structure +

+
+
+ stratum-graph + Knowledge + ActionNode · Capability · GraphRepository trait +
+
+ stratum-state + Operational + PipelineRun · StepRecord · StateTracker trait +
+
+ platform-nats + Transport + JetStream consumer · NKey auth +
+
+ stratum-orchestrator + Coordination + ActionGraph · PipelineContext · StageRunner · auth · + executor +
+
+
+ + +
+

+ Technology Stack +

+
+ Rust + Nickel + Nushell + NATS JetStream + SurrealDB + Cedar + SecretumVault + OCI / Zot + TypeDialog + tokio JoinSet + notify (hot-reload) + oras +
+
+ + +
+

+ Startup Sequence +

+
+
+
🚀
+

+ Boot order — deterministic +

+

+ Each step is a hard dependency for the next. No lazy + initialization, no optional deferred loading. +

+
+ 1. TypeDialog config load 2. SurrealDB connect 3. NATS JetStream + connect 4. OCI Nickel import resolve 5. ActionGraph build 6. + notify watcher start 7. Cedar policies init 8. HTTP server (health + + agent cb) 9. JetStream pull loop +
+
+
+
📊
+

+ Accepted trade-offs +

+

+ nickel export is a subprocess call per file at startup — ~50ms per + node file, mitigated by parallel JoinSet load. Each node execution + spawns a Nushell process — observable latency for sub-second + scripts, acceptable for CI/CD. OCI cold starts require layer + pulls, mitigated by local digest cache. +

+
+
+
+ +
+

+ New workflows — zero orchestrator changes. +

+

+ Add .ncl files. The graph resolves the rest. +

+ Explore Architecture → +
+ +
+

+ StratumIOps — Stratum Orchestrator +

+

+ Graph-driven · Stateless · Auditable · OCI-distributed +

+

+ ADR-003 | Rust + Nickel + Nushell + NATS + SurrealDB +

+
+
+ + + + diff --git a/assets/orchestrator/src/stratumiops-pipeline.html b/assets/orchestrator/src/stratumiops-pipeline.html new file mode 100644 index 0000000..fdebdd9 --- /dev/null +++ b/assets/orchestrator/src/stratumiops-pipeline.html @@ -0,0 +1,186 @@ + + + + + + StratumIOps — Build Pipeline Flow + + + + + +
+ Stratum Build Pipeline Flow — Dark + +
+ + + + diff --git a/assets/orchestrator/stratumiops-arch.html b/assets/orchestrator/stratumiops-arch.html new file mode 100644 index 0000000..8dfd854 --- /dev/null +++ b/assets/orchestrator/stratumiops-arch.html @@ -0,0 +1 @@ +StratumIOps — Orchestrator Architecture
Stratum Orchestrator Architecture — Dark
\ No newline at end of file diff --git a/assets/orchestrator/stratumiops-h.svg b/assets/orchestrator/stratumiops-h.svg new file mode 100644 index 0000000..44e711e --- /dev/null +++ b/assets/orchestrator/stratumiops-h.svg @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + StratumIOps + + + + + + + + + + + + + + + diff --git a/assets/orchestrator/stratumiops-orchestrator.html b/assets/orchestrator/stratumiops-orchestrator.html new file mode 100644 index 0000000..4b87659 --- /dev/null +++ b/assets/orchestrator/stratumiops-orchestrator.html @@ -0,0 +1 @@ + Stratum Orchestrator
StratumIOps

Graph-Driven Workflow Orchestration

Stratum Orchestrator —
Stateless, Agnostic, Auditable

Cross-project event-driven pipelines declared in Nickel, executed by Nushell, coordinated via NATS JetStream, and persisted in SurrealDB. The orchestrator binary never changes when workflows do.

10 Fundamental Design Characteristics

01

Graph-Driven — No Routing Tables

NATS subjects are matched against an ActionGraph built from Nickel node definitions. Each ActionNode declares trigger, input_schemas, output_schemas, and compensate. Adding a workflow means adding .ncl files — zero binary changes.

02

Stateless Orchestrator — DB-First

Every PipelineContext write goes to SurrealDB first, then to an in-memory DashMap cache. On crash, the instance reconstructs from DB. Enables horizontal scaling and crash-free pipeline resumption from last persisted capability.

03

Nickel as Single Source of Truth

Action nodes, capability schemas, and startup config are all defined in Nickel. No DB copy of node definitions exists at runtime. The ActionGraph is built in-memory at startup via nickel export, then kept live via a notify file watcher for hot-reload.

04

Capability Model — DI at Execution Level

Nodes do not depend on each other directly — they declare capabilities they produce and consume. The graph engine resolves dependencies. build-crate does not know about lint-crate; it only needs linted-code. Nodes can be swapped or parallelized without changing consumers.

05

Three Independent Auth Planes

Publisher auth via NATS NKeys (ed25519) controls who can emit events. Workflow authz via Cedar policies controls which pipelines a principal can trigger. Execution credentials via SecretumVault are scoped per-node with TTL equal to the node timeout — revoked on failure.

06

Saga Atomicity — Compensation Not Transactions

Pipelines execute forward through stages. If a stage fails, the orchestrator runs compensate.nu scripts in reverse order through all previously successful stages. Compensation is best-effort; failures are logged and auditable in SurrealDB — the pipeline still reaches Compensated status.

07

Parallel Stages — JoinSet + CancellationToken

Within each stage, nodes with no capability dependencies execute in parallel via tokio::task::JoinSet. Fail-fast is implemented via CancellationToken: the first node failure cancels the token, aborting all sibling tasks in that stage immediately.

08

OCI for Everything — Content-Addressed

Node definitions and the Nickel base library are published as OCI artifacts to a Zot registry. The ncl-import-resolver binary pulls each OCI layer at startup, verifies digest against annotated sha256, then exposes a local path for Nickel imports. Prevents loading unverified definitions.

09

Nushell as Execution Unit — Agnostic

Each node's handler is a Nushell script. The executor spawns nu --no-config-file script.nu, passes PipelineContext inputs as JSON on stdin, reads output JSON from stdout. The orchestrator has no knowledge of what the script does — hot-replaceable without recompiling.

10

TypeDialog — Startup Config Only

TypeDialog is used exclusively for orchestrator startup configuration: SurrealDB URL, NATS URL, Zot URL, Vault URL, log level, feature flags. Not for workflow definitions or node configurations — those live in Nickel. Bounded scope, single responsibility.

Key Architectural Decisions

🕸️

Capability DAG — not a rule router

Topological sort of the capability graph produces a staged execution plan. Each stage is a set of nodes with no inter-dependency. The orchestrator evaluates the graph; it does not hardcode any workflow logic.

lint-crate → produces: linted-code fmt-crate → produces: formatted-code build-crate → consumes: linted-code, formatted-code → produces: built-artifact
🔐

Three-Plane Auth — non-substitutable

NKeys authenticate the publisher on the transport. Cedar authorizes the workflow at the orchestrator boundary. Vault scopes execution credentials per-node with TTL = node timeout, injected as env vars and revoked on failure — never in NATS messages or logs.

Transport: NATS NKeys (ed25519) Authz: Cedar policies Execution: Vault lease, TTL-scoped
🔄

Saga Compensation — auditable rollback

Compensation scripts run in reverse stage order through all previously succeeded stages. The full trace — including compensation failures — is recorded in SurrealDB. 2PC is not feasible across Nushell subprocesses; Saga is the only realistic atomicity model here.

Stage 0: lint ✓ fmt ✓ → executed Stage 1: build ✗ → compensate Stage 0 ←: undo fmt, undo lint

DB-First State — crash recovery is free

PipelineContext is written to SurrealDB before updating the in-memory DashMap. On restart, the cache is reconstructed from the DB. Multiple instances share one SurrealDB — no split-brain, no coordinator needed. Horizontal scaling is structural, not bolted on.

📦

OCI Distribution — verified imports

nickel typecheck → gitleaks detect → nickel export → sha256sum → oras push with content-hash annotations. The ncl-import-resolver verifies each layer's digest against the annotated hash before exposing it to the Nickel import path — tampered definitions cannot load.

🐚

Nushell Executor — domain-agnostic subprocess

Each node spawns nu --no-config-file script.nu. PipelineContext inputs arrive as JSON on stdin; outputs return as JSON on stdout. Each node gets its own process with scoped Vault credentials. Scripts can be tested independently: echo '{}' | nu script.nu.

Architecture & Pipeline Diagrams

Orchestrator Architecture — DarkOrchestrator Architecture — Light

Orchestrator Architecture

Build Pipeline Flow — DarkBuild Pipeline Flow — Light

Build Pipeline Flow

Crate Structure

stratum-graphKnowledgeActionNode · Capability · GraphRepository trait
stratum-stateOperationalPipelineRun · StepRecord · StateTracker trait
platform-natsTransportJetStream consumer · NKey auth
stratum-orchestratorCoordinationActionGraph · PipelineContext · StageRunner · auth · executor

Technology Stack

RustNickelNushellNATS JetStreamSurrealDBCedarSecretumVaultOCI / ZotTypeDialogtokio JoinSetnotify (hot-reload)oras

Startup Sequence

🚀

Boot order — deterministic

Each step is a hard dependency for the next. No lazy initialization, no optional deferred loading.

1. TypeDialog config load 2. SurrealDB connect 3. NATS JetStream connect 4. OCI Nickel import resolve 5. ActionGraph build 6. notify watcher start 7. Cedar policies init 8. HTTP server (health + agent cb) 9. JetStream pull loop
📊

Accepted trade-offs

nickel export is a subprocess call per file at startup — ~50ms per node file, mitigated by parallel JoinSet load. Each node execution spawns a Nushell process — observable latency for sub-second scripts, acceptable for CI/CD. OCI cold starts require layer pulls, mitigated by local digest cache.

New workflows — zero orchestrator changes.

Add .ncl files. The graph resolves the rest.

Explore Architecture →

StratumIOps — Stratum Orchestrator

Graph-driven · Stateless · Auditable · OCI-distributed

ADR-003 | Rust + Nickel + Nushell + NATS + SurrealDB

\ No newline at end of file diff --git a/assets/orchestrator/stratumiops-pipeline.html b/assets/orchestrator/stratumiops-pipeline.html new file mode 100644 index 0000000..70b9606 --- /dev/null +++ b/assets/orchestrator/stratumiops-pipeline.html @@ -0,0 +1 @@ +StratumIOps — Build Pipeline Flow
Stratum Build Pipeline Flow — Dark
\ No newline at end of file diff --git a/assets/orchestrator/w-arch-stratum-orchestrator.svg b/assets/orchestrator/w-arch-stratum-orchestrator.svg new file mode 100644 index 0000000..59e2814 --- /dev/null +++ b/assets/orchestrator/w-arch-stratum-orchestrator.svg @@ -0,0 +1,469 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +STRATUM ORCHESTRATOR +Event-Driven · Graph-Guided · Atomic Execution · Stateless · OCI-Native + + +EVENT SOURCES + + + + +provisioning +emits dev.crate.> +crate-modified · deploy + + + + +kogral +emits dev.knowledge.> +node-updated · indexed + + + + +syntaxis +emits dev.project.> +phase · task-completed + + + + +stratumiops +emits dev.model.> +llm-call · embed-request + + + + +typedialog +emits dev.form.> +submitted · validated + + ++ more ... + + + + + + + + + + + + + + + + + + +NATS +JetStream +dev.> + + + + + + + + + + + + + + + + + +NKey verify +ed25519 JWT + + + + + + +STRATUM ORCHESTRATOR +Agnostic · Stateless · Graph-Guided + + + + +◈ ActionGraph +in-memory · Nickel nodes +topo-sort · cycle-detect + + + +⬡ PipelineCtx +DB-first · typed caps +schema-validated + + + +▶ StageRunner +JoinSet · parallel stages +CancellationToken +retry + backoff on failure +saga compensate.nu + + + +⚙ RuleEngine + +Cedar + +◈ Cedar +permit · forbid · conditions +per-node authz policies + +◈ NKey +ed25519 asymmetric keys +JWT per-process · verify + +↺ Saga rollback on failure · compensate.nu in reverse + + + + + + + + + + +SurrealDB +Pipeline state · Step results +orchestrator_state ns +crash recovery + + + + + +SecretumVault +Credentials · TTL leases +vault:/secret/... +never in NATS payload + + + + + +Zot OCI Registry +Node defs · Nickel libs +oci://registry/nodes/ +content-addressed · signed + + +OPTIONAL + + + + + + + +Kogral +Knowledge graph +node-updated triggers + +optional + + + +Syntaxis +Project orchestration +phase-transition events + +optional + + + +TypeDialog +Service config UI +startup config NCL only + + + + + + + + +Git repo +Git events → NATS +webhook → dev.crate.> +push · tag · pr + + + + + + + + +Nu Executor +Atomic steps · Pure functions +scripts/nu/*.nu +stdout=output · exit-code=status + + + + +AI Agent +stratum-llm · NATS protocol +dev.agent.*.requested/responded +oneshot correlation · timeout + + + + + +Nickel Base Library +OCI-published · content-addressed · build-verified · typecheck-gated +orchestrator-types.ncl · capability-schemas.ncl · defaults.ncl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + StratumIOps + + + + + + + +LEGEND + +event flow (NATS) + +auth / credentials + +execution + +state (DB) + +OCI / registry + +optional + + +stratumiops · v0.1 + diff --git a/assets/orchestrator/w-flow-stratum-build-pipeline.svg b/assets/orchestrator/w-flow-stratum-build-pipeline.svg new file mode 100644 index 0000000..ea9e4cb --- /dev/null +++ b/assets/orchestrator/w-flow-stratum-build-pipeline.svg @@ -0,0 +1,450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +STRATUM ORCHESTRATOR · BUILD PIPELINE FLOW +event: dev.crate.provisioning-cli.modified · trigger → ActionGraph → stages → state → emit + + + + + + + + +Developer · Forgejo +git push / webhook + + + + +NATS JetStream +Event Bus + + + + +Orchestrator +Planning + + + + +Auth · NKeys ++ Cedar + + + + +Stage 0 +‖ Parallel + + + + +Stage 1 +→ Sequential + + + + +Stage 2 +→ Sequential + + + + +SurrealDB +State + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + git push + code change + commit: abc123f + + + + + + 2 + + dev.crate. + provisioning-cli + .modified + + {"crate":"prvng-cli", + "sha":"abc123f"} + + + + + + 3 + + pull_batch + (10) + JetStream consumer + + + + + + 4 + + NKey + verify + publisher auth + + + + + + 5 + + ActionGraph + query + topo-sort + + + [lint-crate, fmt-crate] + → build-crate + → install + + + Stages + + + + + + 6 + + Cedar + authorize + permit: trigger + node:build-crate + + + + + +‖ parallel · JoinSet + + + 7 + + + + + + lint.nu + (clippy) + + fmt.nu + (cargo fmt) + + + +join_all → PipelineContext + + + + + 8 + + deposit: + linted-code + formatted-code + + + + + + 9 + + build.nu + (cargo build) + + + 🔐 Vault cred + TTL=step timeout + deposit: built-artifact + + + + + + 10 + + install.nu + extracts: built-artifact + deposit: installed + + + + + + 11 + + pipeline_run + status: success + all steps persisted + + + + + + 12 + + dev.crate. + provisioning-cli + .built + duration_ms, status + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +rollback path: compensate.nu in reverse order + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + StratumIOps + + + + + + +Legend: + + +NATS events + + +Orchestrator + + +Auth (NKeys + Cedar) + + +Execution (Stage 0) + + +State (SurrealDB) + + +Rollback path + +StratumIOps · stratum-orchestrator +stratum-orchestrator · build pipeline + diff --git a/config/cedar/permit-orchestrator-execute.cedar b/config/cedar/permit-orchestrator-execute.cedar new file mode 100644 index 0000000..2b7795a --- /dev/null +++ b/config/cedar/permit-orchestrator-execute.cedar @@ -0,0 +1,6 @@ +// Permit the orchestrator principal to execute any node. +permit( + principal == User::"orchestrator", + action == Action::"execute", + resource in ResourceGroup::"nodes" +); diff --git a/config/orchestrator-config.ncl b/config/orchestrator-config.ncl new file mode 100644 index 0000000..92c9020 --- /dev/null +++ b/config/orchestrator-config.ncl @@ -0,0 +1,18 @@ +{ + surrealdb_url | String = "ws://localhost:8100", + nats_url | String = "nats://localhost:4222", + zot_url | String = "localhost:5000", + vault_url | String = "http://localhost:9094", + action_nodes_dir | String = "./action-nodes", + schemas_dir | String = "./schemas/capabilities", + cedar_policy_dir | String = "./config/cedar", + log_level | [| 'trace, 'debug, 'info, 'warn, 'error |] = 'info, + http_port | Number = 9088, + trusted_nkeys | Array String = [], + features = { + nkey_auth_required | Bool = false, + cedar_auth_required | Bool = true, + schema_validation | Bool = true, + agent_executor_enabled | Bool = false, + }, +} diff --git a/config/zot-config.json b/config/zot-config.json new file mode 100644 index 0000000..7665c76 --- /dev/null +++ b/config/zot-config.json @@ -0,0 +1,18 @@ +{ + "distSpecVersion": "1.1.0", + "storage": { + "rootDirectory": "/var/lib/registry" + }, + "http": { + "address": "0.0.0.0", + "port": "5000" + }, + "log": { + "level": "info" + }, + "extensions": { + "search": { + "enable": true + } + } +} diff --git a/crates/ncl-import-resolver/Cargo.toml b/crates/ncl-import-resolver/Cargo.toml new file mode 100644 index 0000000..963ea36 --- /dev/null +++ b/crates/ncl-import-resolver/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ncl-import-resolver" +version = "0.1.0" +edition.workspace = true +description = "Resolve OCI-hosted Nickel library imports to local filesystem cache" +license.workspace = true + +[[bin]] +name = "ncl-import-resolver" +path = "src/main.rs" + +[dependencies] +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +dirs = { workspace = true } diff --git a/crates/ncl-import-resolver/src/main.rs b/crates/ncl-import-resolver/src/main.rs new file mode 100644 index 0000000..16cde68 --- /dev/null +++ b/crates/ncl-import-resolver/src/main.rs @@ -0,0 +1,131 @@ +use std::{ + path::{Path, PathBuf}, + process::Command, +}; + +use anyhow::{anyhow, Context, Result}; +use serde::{Deserialize, Serialize}; +use tracing::info; + +/// One entry in `resolver-manifest.json`. +#[derive(Debug, Serialize, Deserialize)] +struct ManifestEntry { + name: String, + registry: String, + tag: String, + /// Expected sha256 of the exported JSON (annotated on the OCI artifact) + expected_sha256: Option, +} + +fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter(std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string())) + .init(); + + let manifest_path = std::env::args() + .nth(1) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("resolver-manifest.json")); + + let manifest = load_manifest(&manifest_path)?; + let cache_base = cache_dir()?; + + for entry in &manifest { + resolve_entry(entry, &cache_base)?; + } + + info!("all Nickel imports resolved"); + Ok(()) +} + +fn load_manifest(path: &Path) -> Result> { + let content = + std::fs::read_to_string(path).with_context(|| format!("reading manifest '{}'", path.display()))?; + serde_json::from_str(&content).context("parsing resolver manifest JSON") +} + +fn cache_dir() -> Result { + let base = dirs::cache_dir() + .ok_or_else(|| anyhow!("could not determine cache directory"))?; + let dir = base.join("stratum").join("ncl"); + std::fs::create_dir_all(&dir) + .with_context(|| format!("creating cache dir '{}'", dir.display()))?; + Ok(dir) +} + +fn resolve_entry(entry: &ManifestEntry, cache_base: &Path) -> Result<()> { + let image_ref = format!("{}/{}:{}", entry.registry, entry.name, entry.tag); + let dest_dir = cache_base.join(&entry.name).join(&entry.tag); + + if dest_dir.exists() { + info!("cache hit: {} → {}", image_ref, dest_dir.display()); + symlink_to_nickel_dir(&entry.name, &dest_dir)?; + return Ok(()); + } + + info!("pulling: {image_ref}"); + std::fs::create_dir_all(&dest_dir)?; + + let export_file = dest_dir.join(format!("{}.json", entry.name)); + + let output = Command::new("oras") + .arg("pull") + .arg("--output") + .arg(&dest_dir) + .arg(&image_ref) + .output() + .with_context(|| format!("running oras pull for '{image_ref}'"))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!("oras pull failed for '{image_ref}': {stderr}")); + } + + // Verify sha256 if expected + if let Some(expected) = &entry.expected_sha256 { + let actual = sha256_file(&export_file)?; + if actual != *expected { + std::fs::remove_dir_all(&dest_dir)?; + return Err(anyhow!( + "sha256 mismatch for '{image_ref}': expected {expected}, got {actual}" + )); + } + info!("sha256 verified: {actual}"); + } + + symlink_to_nickel_dir(&entry.name, &dest_dir)?; + Ok(()) +} + +fn symlink_to_nickel_dir(name: &str, cache_dir: &Path) -> Result<()> { + let link = PathBuf::from("nickel").join(name); + + if link.exists() || link.is_symlink() { + std::fs::remove_file(&link) + .with_context(|| format!("removing existing symlink '{}'", link.display()))?; + } + + std::os::unix::fs::symlink(cache_dir, &link) + .with_context(|| format!("creating symlink '{}' → '{}'", link.display(), cache_dir.display()))?; + + info!("symlinked: {} → {}", link.display(), cache_dir.display()); + Ok(()) +} + +fn sha256_file(path: &Path) -> Result { + let output = Command::new("sha256sum") + .arg(path) + .output() + .with_context(|| format!("running sha256sum on '{}'", path.display()))?; + + if !output.status.success() { + return Err(anyhow!("sha256sum failed on '{}'", path.display())); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + stdout + .split_whitespace() + .next() + .map(|s| s.to_string()) + .ok_or_else(|| anyhow!("unexpected sha256sum output: {stdout}")) +} diff --git a/crates/platform-nats/Cargo.toml b/crates/platform-nats/Cargo.toml new file mode 100644 index 0000000..556d7a1 --- /dev/null +++ b/crates/platform-nats/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "platform-nats" +version = "0.1.0" +edition.workspace = true +description = "NATS JetStream client with NKey authentication for stratum services" +license.workspace = true + +[dependencies] +async-nats = { workspace = true } +nkeys = { workspace = true } +anyhow = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +bytes = "1.9" +futures = { workspace = true } + +[dev-dependencies] +tokio-test = { workspace = true } diff --git a/crates/platform-nats/src/auth.rs b/crates/platform-nats/src/auth.rs new file mode 100644 index 0000000..71a5a0d --- /dev/null +++ b/crates/platform-nats/src/auth.rs @@ -0,0 +1,112 @@ +use std::collections::HashSet; + +use anyhow::{anyhow, Result}; +use bytes::Bytes; + +pub struct NKeyAuth { + trusted_public_keys: HashSet, +} + +impl NKeyAuth { + pub fn new(trusted_public_keys: Vec) -> Self { + Self { + trusted_public_keys: trusted_public_keys.into_iter().collect(), + } + } + + /// Verify an incoming NATS message. + /// + /// Expects: + /// - Header `Nats-Nkey`: ed25519 public key (base32/nkeys encoding) + /// - Header `Nats-Signature`: base64url signature of the message payload + /// + /// Returns `Ok(())` if the message is trusted and the signature is valid. + /// Returns `Err` if missing, untrusted, or signature invalid. + pub fn verify_message( + &self, + headers: Option<&async_nats::header::HeaderMap>, + payload: &Bytes, + ) -> Result<()> { + let headers = + headers.ok_or_else(|| anyhow!("message has no headers — Nats-Nkey required"))?; + + let nkey_header = headers + .get("Nats-Nkey") + .ok_or_else(|| anyhow!("missing Nats-Nkey header"))?; + let nkey_str = nkey_header.as_str().trim(); + + if !self.trusted_public_keys.contains(nkey_str) { + return Err(anyhow!("untrusted publisher nkey: {nkey_str}")); + } + + let sig_header = headers + .get("Nats-Signature") + .ok_or_else(|| anyhow!("missing Nats-Signature header"))?; + let sig_b64 = sig_header.as_str().trim(); + + let sig_bytes = base64_url_decode(sig_b64) + .map_err(|e| anyhow!("invalid Nats-Signature base64: {e}"))?; + + let kp = nkeys::KeyPair::from_public_key(nkey_str) + .map_err(|e| anyhow!("invalid nkey public key: {e}"))?; + + kp.verify(payload, &sig_bytes) + .map_err(|e| anyhow!("signature verification failed: {e}"))?; + + Ok(()) + } + + pub fn is_auth_required(&self) -> bool { + !self.trusted_public_keys.is_empty() + } +} + +fn base64_url_decode(input: &str) -> Result> { + let standard: String = input + .chars() + .map(|c| match c { + '-' => '+', + '_' => '/', + c => c, + }) + .collect(); + + // Pad to multiple of 4 + let pad = (4 - standard.len() % 4) % 4; + let padded = format!("{}{}", standard, "=".repeat(pad)); + + (0..padded.len()) + .step_by(4) + .try_fold(Vec::new(), |mut acc, i| { + let chunk = &padded[i..(i + 4).min(padded.len())]; + let decoded = base64_chunk(chunk)?; + acc.extend_from_slice(&decoded); + Ok::, anyhow::Error>(acc) + }) +} + +fn base64_chunk(chunk: &str) -> Result> { + // Simple base64 decode for a 4-char chunk + let b64_chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let mut val: u32 = 0; + let mut valid = 0u32; + + for c in chunk.bytes() { + if c == b'=' { + break; + } + let pos = b64_chars + .iter() + .position(|&b| b == c) + .ok_or_else(|| anyhow!("invalid base64 character: {c}"))?; + val = (val << 6) | pos as u32; + valid += 6; + } + + let mut result = Vec::new(); + while valid >= 8 { + valid -= 8; + result.push(((val >> valid) & 0xFF) as u8); + } + Ok(result) +} diff --git a/crates/platform-nats/src/client.rs b/crates/platform-nats/src/client.rs new file mode 100644 index 0000000..4d8c386 --- /dev/null +++ b/crates/platform-nats/src/client.rs @@ -0,0 +1,179 @@ +use anyhow::{anyhow, Context, Result}; +use async_nats::{ + jetstream::{ + self, consumer::pull::Config as PullConfig, stream::Config as StreamConfig, + Message as JsMessage, + }, + Client, +}; +use bytes::Bytes; +use futures::StreamExt; +use serde::{Deserialize, Serialize}; +use tracing::{debug, warn}; + +use crate::auth::NKeyAuth; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NatsConfig { + pub url: String, + /// ed25519 NKey seed for signing published messages (optional) + pub nkey_seed: Option, + pub stream_name: String, + pub consumer_name: String, + pub subjects: Vec, + /// If true, reject messages that lack valid NKey signatures + pub require_signed_messages: bool, + pub trusted_nkeys: Vec, +} + +pub struct EventStream { + client: Client, + consumer: jetstream::consumer::Consumer, + auth: NKeyAuth, + signing_keypair: Option, +} + +impl EventStream { + pub async fn connect(cfg: &NatsConfig) -> Result { + let client = async_nats::connect(&cfg.url) + .await + .with_context(|| format!("connecting to NATS at {}", cfg.url))?; + + let js = jetstream::new(client.clone()); + + // Ensure stream exists with the configured subjects + js.get_or_create_stream(StreamConfig { + name: cfg.stream_name.clone(), + subjects: cfg.subjects.clone(), + ..Default::default() + }) + .await + .with_context(|| format!("creating/getting stream '{}'", cfg.stream_name))?; + + let stream = js + .get_stream(&cfg.stream_name) + .await + .with_context(|| format!("getting stream '{}'", cfg.stream_name))?; + + let consumer = stream + .get_or_create_consumer( + &cfg.consumer_name, + PullConfig { + durable_name: Some(cfg.consumer_name.clone()), + ..Default::default() + }, + ) + .await + .with_context(|| format!("creating/getting consumer '{}'", cfg.consumer_name))?; + + let auth = NKeyAuth::new(cfg.trusted_nkeys.clone()); + + let signing_keypair = cfg + .nkey_seed + .as_deref() + .map(|seed| nkeys::KeyPair::from_seed(seed).context("parsing NKey seed")) + .transpose()?; + + Ok(Self { + client, + consumer, + auth, + signing_keypair, + }) + } + + /// Pull up to `max_msgs` messages. Returns (subject, payload, Message) tuples. + /// Messages that fail auth verification are nack'd and excluded from the result. + pub async fn pull_batch(&self, max_msgs: usize) -> Result> { + let mut messages = self + .consumer + .fetch() + .max_messages(max_msgs) + .messages() + .await + .context("pulling message batch")?; + + let mut result = Vec::new(); + + while let Some(msg) = messages.next().await { + let msg: JsMessage = msg.map_err(|e| anyhow!("receiving message from stream: {e}"))?; + let subject = msg.subject.to_string(); + let payload = msg.payload.clone(); + + if self.auth.is_auth_required() { + if let Err(e) = self.auth.verify_message(msg.headers.as_ref(), &payload) { + warn!("dropping message on '{subject}': {e}"); + // Ack to remove from queue — do not block the stream with bad messages + let _ = msg.ack().await; + continue; + } + } + + debug!("accepted message on '{subject}' ({} bytes)", payload.len()); + result.push((subject, payload, msg)); + } + + Ok(result) + } + + /// Publish a message, signing it with the configured NKey if available. + pub async fn publish(&self, subject: &str, payload: Bytes) -> Result<()> { + let mut headers = async_nats::header::HeaderMap::new(); + + if let Some(kp) = &self.signing_keypair { + let pub_key = kp.public_key(); + let signature = kp + .sign(&payload) + .map_err(|e| anyhow!("signing message: {e}"))?; + let sig_b64 = base64_url_encode(&signature); + + headers.insert("Nats-Nkey", pub_key.as_str()); + headers.insert("Nats-Signature", sig_b64.as_str()); + } + + self.client + .publish_with_headers(subject.to_owned(), headers, payload) + .await + .with_context(|| format!("publishing to '{subject}'"))?; + + Ok(()) + } + + pub fn client(&self) -> &Client { + &self.client + } +} + +fn base64_url_encode(data: &[u8]) -> String { + const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let mut out = String::with_capacity(data.len().div_ceil(3) * 4); + + for chunk in data.chunks(3) { + let b0 = chunk[0] as usize; + let b1 = chunk.get(1).copied().unwrap_or(0) as usize; + let b2 = chunk.get(2).copied().unwrap_or(0) as usize; + + let combined = (b0 << 16) | (b1 << 8) | b2; + out.push(CHARS[(combined >> 18) & 0x3F] as char); + out.push(CHARS[(combined >> 12) & 0x3F] as char); + out.push(if chunk.len() > 1 { + CHARS[(combined >> 6) & 0x3F] as char + } else { + '=' + }); + out.push(if chunk.len() > 2 { + CHARS[combined & 0x3F] as char + } else { + '=' + }); + } + + // Convert to URL-safe base64 + out.chars() + .map(|c| match c { + '+' => '-', + '/' => '_', + c => c, + }) + .collect() +} diff --git a/crates/platform-nats/src/lib.rs b/crates/platform-nats/src/lib.rs new file mode 100644 index 0000000..3525b94 --- /dev/null +++ b/crates/platform-nats/src/lib.rs @@ -0,0 +1,5 @@ +pub mod auth; +pub mod client; + +pub use auth::NKeyAuth; +pub use client::{EventStream, NatsConfig}; diff --git a/crates/platform-nats/tests/test_nkey_auth.rs b/crates/platform-nats/tests/test_nkey_auth.rs new file mode 100644 index 0000000..fc86bb0 --- /dev/null +++ b/crates/platform-nats/tests/test_nkey_auth.rs @@ -0,0 +1,148 @@ +/// Integration test: NKey authentication sign/verify round-trip. +/// +/// Tests the full flow: key generation → signing → header injection → verification. +use async_nats::header::HeaderMap; +use bytes::Bytes; +use platform_nats::NKeyAuth; + +/// Produce a `HeaderMap` with NKey authentication headers for the given payload. +fn signed_headers(seed: &str, payload: &[u8]) -> HeaderMap { + let kp = nkeys::KeyPair::from_seed(seed).unwrap(); + let sig = kp.sign(payload).unwrap(); + let sig_b64 = b64url_encode(&sig); + + let pub_key = kp.public_key(); + let mut headers = HeaderMap::new(); + headers.insert("Nats-Nkey", pub_key.as_str()); + headers.insert("Nats-Signature", sig_b64.as_str()); + headers +} + +/// Base64url encode (no padding, URL-safe alphabet: `-` and `_`). +/// +/// Packs input into 3-byte chunks, encodes each as four 6-bit Base64 characters. +/// Partial trailing chunks emit 2 or 3 characters (no `=` padding). +fn b64url_encode(data: &[u8]) -> String { + const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + let mut out = String::with_capacity(data.len().div_ceil(3) * 4); + for chunk in data.chunks(3) { + let b0 = chunk[0] as u32; + let b1 = chunk.get(1).copied().unwrap_or(0) as u32; + let b2 = chunk.get(2).copied().unwrap_or(0) as u32; + let n = (b0 << 16) | (b1 << 8) | b2; + out.push(CHARS[((n >> 18) & 0x3F) as usize] as char); + out.push(CHARS[((n >> 12) & 0x3F) as usize] as char); + if chunk.len() > 1 { + out.push(CHARS[((n >> 6) & 0x3F) as usize] as char); + } + if chunk.len() > 2 { + out.push(CHARS[(n & 0x3F) as usize] as char); + } + } + out +} + +/// Generate a fresh user NKey seed for testing. +fn fresh_seed() -> String { + let kp = nkeys::KeyPair::new_user(); + kp.seed().unwrap() +} + +#[test] +fn test_trusted_key_signature_accepted() { + let seed = fresh_seed(); + let kp = nkeys::KeyPair::from_seed(&seed).unwrap(); + let pub_key = kp.public_key(); + + let auth = NKeyAuth::new(vec![pub_key]); + let payload = Bytes::from_static(b"hello stratum"); + let headers = signed_headers(&seed, &payload); + + auth.verify_message(Some(&headers), &payload).unwrap(); +} + +#[test] +fn test_untrusted_key_rejected() { + let trusted_seed = fresh_seed(); + let trusted_kp = nkeys::KeyPair::from_seed(&trusted_seed).unwrap(); + let auth = NKeyAuth::new(vec![trusted_kp.public_key()]); + + // Sign with a different (untrusted) key + let attacker_seed = fresh_seed(); + let payload = Bytes::from_static(b"malicious payload"); + let headers = signed_headers(&attacker_seed, &payload); + + let err = auth.verify_message(Some(&headers), &payload).unwrap_err(); + assert!( + err.to_string().contains("untrusted publisher"), + "expected untrusted error, got: {err}" + ); +} + +#[test] +fn test_tampered_payload_rejected() { + let seed = fresh_seed(); + let kp = nkeys::KeyPair::from_seed(&seed).unwrap(); + let pub_key = kp.public_key(); + + let auth = NKeyAuth::new(vec![pub_key]); + let original_payload = Bytes::from_static(b"original payload"); + let headers = signed_headers(&seed, &original_payload); + + // Signature was for `original_payload`, but we verify against tampered one + let tampered = Bytes::from_static(b"tampered payload!!!"); + let err = auth + .verify_message(Some(&headers), &tampered) + .unwrap_err(); + assert!( + err.to_string().contains("signature verification failed") + || err.to_string().contains("untrusted"), + "expected signature error, got: {err}" + ); +} + +#[test] +fn test_missing_headers_rejected() { + let auth = NKeyAuth::new(vec!["some-key".to_string()]); + let payload = Bytes::from_static(b"payload"); + + let err = auth.verify_message(None, &payload).unwrap_err(); + assert!( + err.to_string().contains("no headers"), + "expected no-headers error, got: {err}" + ); +} + +#[test] +fn test_missing_nkey_header_rejected() { + let auth = NKeyAuth::new(vec!["some-key".to_string()]); + let payload = Bytes::from_static(b"payload"); + + let headers = HeaderMap::new(); // empty — no Nats-Nkey + let err = auth + .verify_message(Some(&headers), &payload) + .unwrap_err(); + assert!( + err.to_string().contains("missing Nats-Nkey"), + "expected missing-header error, got: {err}" + ); +} + +#[test] +fn test_auth_not_required_when_no_trusted_keys() { + let auth = NKeyAuth::new(vec![]); // no trusted keys = auth not required + assert!( + !auth.is_auth_required(), + "empty trusted set must mean auth is not required" + ); +} + +#[test] +fn test_auth_required_when_trusted_keys_present() { + let kp = nkeys::KeyPair::new_user(); + let auth = NKeyAuth::new(vec![kp.public_key()]); + assert!( + auth.is_auth_required(), + "non-empty trusted set must mean auth is required" + ); +} diff --git a/crates/stratum-embeddings/Cargo.toml b/crates/stratum-embeddings/Cargo.toml index a0eb10c..45733c3 100644 --- a/crates/stratum-embeddings/Cargo.toml +++ b/crates/stratum-embeddings/Cargo.toml @@ -22,8 +22,6 @@ humantime-serde = { workspace = true } # Caching moka = { workspace = true } -# Persistent cache (optional) -sled = { workspace = true, optional = true } # Local embeddings fastembed = { workspace = true, optional = true } @@ -66,12 +64,12 @@ all-providers = [ # Cache backends memory-cache = [] -persistent-cache = ["sled"] +persistent-cache = ["surrealdb", "surrealdb/kv-rocksdb"] all-cache = ["memory-cache", "persistent-cache"] # Vector storage backends lancedb-store = ["lancedb", "arrow"] -surrealdb-store = ["surrealdb"] +surrealdb-store = ["surrealdb", "surrealdb/kv-rocksdb"] all-stores = ["lancedb-store", "surrealdb-store"] # Observability diff --git a/crates/stratum-embeddings/src/cache/persistent.rs b/crates/stratum-embeddings/src/cache/persistent.rs index eb6d328..7297d31 100644 --- a/crates/stratum-embeddings/src/cache/persistent.rs +++ b/crates/stratum-embeddings/src/cache/persistent.rs @@ -1,96 +1,122 @@ -#[cfg(feature = "persistent-cache")] +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; use std::path::Path; -#[cfg(feature = "persistent-cache")] use async_trait::async_trait; -#[cfg(feature = "persistent-cache")] -use sled::Db; +use surrealdb::{ + engine::local::{Db, Mem, RocksDb}, + Surreal, +}; -#[cfg(feature = "persistent-cache")] use crate::{cache::EmbeddingCache, error::EmbeddingError, traits::Embedding}; pub struct PersistentCache { - db: Db, + db: Surreal, + size_hint: Arc, } impl PersistentCache { - pub fn new>(path: P) -> Result { - let db = sled::open(path) - .map_err(|e| EmbeddingError::CacheError(format!("Failed to open sled db: {}", e)))?; - - Ok(Self { db }) - } - - pub fn in_memory() -> Result { - let db = sled::Config::new().temporary(true).open().map_err(|e| { - EmbeddingError::CacheError(format!("Failed to create in-memory sled db: {}", e)) + /// Persistent RocksDB-backed cache. Survives restarts. + pub async fn new(path: impl AsRef) -> Result { + let db = Surreal::new::(path.as_ref()).await.map_err(|e| { + EmbeddingError::CacheError(format!("RocksDB open failed: {e}")) })?; - - Ok(Self { db }) - } - - fn serialize_embedding(embedding: &Embedding) -> Result, EmbeddingError> { - serde_json::to_vec(embedding) - .map_err(|e| EmbeddingError::SerializationError(format!("Embedding serialize: {}", e))) - } - - fn deserialize_embedding(data: &[u8]) -> Result { - serde_json::from_slice(data).map_err(|e| { - EmbeddingError::SerializationError(format!("Embedding deserialize: {}", e)) + db.use_ns("stratum") + .use_db("embeddings") + .await + .map_err(|e| EmbeddingError::CacheError(format!("Namespace select failed: {e}")))?; + let count = Self::db_count(&db).await?; + Ok(Self { + db, + size_hint: Arc::new(AtomicUsize::new(count)), }) } + + /// In-memory instance for tests and ephemeral use (kv-mem). + pub async fn in_memory() -> Result { + let db = Surreal::new::(()).await.map_err(|e| { + EmbeddingError::CacheError(format!("In-memory SurrealDB init failed: {e}")) + })?; + db.use_ns("stratum") + .use_db("embeddings") + .await + .map_err(|e| EmbeddingError::CacheError(format!("Namespace select failed: {e}")))?; + Ok(Self { + db, + size_hint: Arc::new(AtomicUsize::new(0)), + }) + } + + async fn db_count(db: &Surreal) -> Result { + let rows: Vec = db + .query("SELECT count() FROM embedding_cache_v1 GROUP ALL") + .await + .map_err(|e| EmbeddingError::CacheError(format!("Count query failed: {e}")))? + .take(0) + .map_err(|e| EmbeddingError::CacheError(format!("Count result extraction failed: {e}")))?; + Ok(rows + .into_iter() + .next() + .and_then(|v| v.get("count").and_then(|c| c.as_u64())) + .unwrap_or(0) as usize) + } } #[async_trait] impl EmbeddingCache for PersistentCache { async fn get(&self, key: &str) -> Option { - self.db - .get(key) - .ok() - .flatten() - .and_then(|bytes| Self::deserialize_embedding(&bytes).ok()) + let raw: Option = self + .db + .select(("embedding_cache_v1", key)) + .await + .ok()?; + raw?.get("vector") + .and_then(|v| serde_json::from_value(v.clone()).ok()) } async fn insert(&self, key: &str, embedding: Embedding) { - if let Ok(bytes) = Self::serialize_embedding(&embedding) { - let _ = self.db.insert(key, bytes); - let _ = self.db.flush(); + let payload = serde_json::json!({ "vector": embedding }); + let result: Result, _> = self + .db + .upsert(("embedding_cache_v1", key)) + .content(payload) + .await; + if result.is_ok() { + self.size_hint.fetch_add(1, Ordering::Relaxed); } } async fn get_batch(&self, keys: &[String]) -> Vec> { - keys.iter() - .map(|key| { - self.db - .get(key) - .ok() - .flatten() - .and_then(|bytes| Self::deserialize_embedding(&bytes).ok()) - }) - .collect() + let mut out = Vec::with_capacity(keys.len()); + for key in keys { + out.push(self.get(key.as_str()).await); + } + out } async fn insert_batch(&self, items: Vec<(String, Embedding)>) { for (key, embedding) in items { - if let Ok(bytes) = Self::serialize_embedding(&embedding) { - let _ = self.db.insert(key, bytes); - } + self.insert(key.as_str(), embedding).await; } - let _ = self.db.flush(); } async fn invalidate(&self, key: &str) { - let _ = self.db.remove(key); - let _ = self.db.flush(); + let result: Result, _> = + self.db.delete(("embedding_cache_v1", key)).await; + if result.map(|opt| opt.is_some()).unwrap_or(false) { + self.size_hint.fetch_sub(1, Ordering::Relaxed); + } } async fn clear(&self) { - let _ = self.db.clear(); - let _ = self.db.flush(); + let _: Result, _> = self.db.delete("embedding_cache_v1").await; + self.size_hint.store(0, Ordering::Relaxed); } fn size(&self) -> usize { - self.db.len() + self.size_hint.load(Ordering::Relaxed) } } @@ -100,7 +126,7 @@ mod tests { #[tokio::test] async fn test_persistent_cache_in_memory() { - let cache = PersistentCache::in_memory().expect("Failed to create cache"); + let cache = PersistentCache::in_memory().await.expect("Failed to create cache"); let embedding = vec![1.0, 2.0, 3.0]; cache.insert("test_key", embedding.clone()).await; @@ -111,13 +137,12 @@ mod tests { #[tokio::test] async fn test_persistent_cache_batch() { - let cache = PersistentCache::in_memory().expect("Failed to create cache"); + let cache = PersistentCache::in_memory().await.expect("Failed to create cache"); let items = vec![ ("key1".to_string(), vec![1.0, 2.0]), ("key2".to_string(), vec![3.0, 4.0]), ]; - cache.insert_batch(items).await; let keys = vec!["key1".to_string(), "key2".to_string()]; @@ -129,7 +154,7 @@ mod tests { #[tokio::test] async fn test_persistent_cache_invalidate() { - let cache = PersistentCache::in_memory().expect("Failed to create cache"); + let cache = PersistentCache::in_memory().await.expect("Failed to create cache"); cache.insert("key1", vec![1.0]).await; assert!(cache.get("key1").await.is_some()); @@ -140,11 +165,10 @@ mod tests { #[tokio::test] async fn test_persistent_cache_clear() { - let cache = PersistentCache::in_memory().expect("Failed to create cache"); + let cache = PersistentCache::in_memory().await.expect("Failed to create cache"); cache.insert("key1", vec![1.0]).await; cache.insert("key2", vec![2.0]).await; - assert_eq!(cache.size(), 2); cache.clear().await; assert_eq!(cache.size(), 0); diff --git a/crates/stratum-embeddings/src/error.rs b/crates/stratum-embeddings/src/error.rs index eadb61d..2991ac5 100644 --- a/crates/stratum-embeddings/src/error.rs +++ b/crates/stratum-embeddings/src/error.rs @@ -66,7 +66,10 @@ impl From for EmbeddingError { if err.is_timeout() { Self::Timeout(err.to_string()) } else if err.is_status() { - Self::HttpError(format!("HTTP {}: {}", err.status().unwrap(), err)) + Self::HttpError(format!( + "HTTP {}: {err}", + err.status().map_or_else(|| "".to_string(), |s| s.to_string()) + )) } else { Self::ApiError(err.to_string()) } diff --git a/crates/stratum-embeddings/src/store/surrealdb.rs b/crates/stratum-embeddings/src/store/surrealdb.rs index 9af3bda..f2264ae 100644 --- a/crates/stratum-embeddings/src/store/surrealdb.rs +++ b/crates/stratum-embeddings/src/store/surrealdb.rs @@ -1,11 +1,12 @@ +#[cfg(feature = "surrealdb-store")] +use std::path::Path; + #[cfg(feature = "surrealdb-store")] use async_trait::async_trait; -#[cfg(feature = "surrealdb-store")] -use serde::{Deserialize, Serialize}; + #[cfg(feature = "surrealdb-store")] use surrealdb::{ - engine::local::{Db, Mem}, - sql::Thing, + engine::local::{Db, Mem, RocksDb}, Surreal, }; @@ -16,13 +17,6 @@ use crate::{ traits::Embedding, }; -#[derive(Debug, Serialize, Deserialize)] -struct EmbeddingRecord { - id: Option, - vector: Vec, - metadata: serde_json::Value, -} - pub struct SurrealDbStore { db: Surreal, table: String, @@ -42,21 +36,37 @@ impl SurrealDbStore { }) } + /// RocksDB-backed persistent store. + pub async fn new_rocksdb( + path: impl AsRef, + table_name: &str, + config: VectorStoreConfig, + ) -> Result { + let db = Surreal::new::(path.as_ref()).await.map_err(|e| { + EmbeddingError::Initialization(format!("RocksDB SurrealDB connection failed: {e}")) + })?; + db.use_ns("embeddings") + .use_db("vectors") + .await + .map_err(|e| { + EmbeddingError::Initialization(format!("Failed to set namespace: {e}")) + })?; + Self::new(db, table_name, config).await + } + pub async fn new_memory( table_name: &str, config: VectorStoreConfig, ) -> Result { let db = Surreal::new::(()).await.map_err(|e| { - EmbeddingError::Initialization(format!("SurrealDB connection failed: {}", e)) + EmbeddingError::Initialization(format!("SurrealDB connection failed: {e}")) })?; - db.use_ns("embeddings") .use_db("embeddings") .await .map_err(|e| { - EmbeddingError::Initialization(format!("Failed to set namespace: {}", e)) + EmbeddingError::Initialization(format!("Failed to set namespace: {e}")) })?; - Self::new(db, table_name, config).await } @@ -64,17 +74,31 @@ impl SurrealDbStore { if a.len() != b.len() { return 0.0; } - let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum(); let mag_a: f32 = a.iter().map(|x| x * x).sum::().sqrt(); let mag_b: f32 = b.iter().map(|x| x * x).sum::().sqrt(); - if mag_a > 0.0 && mag_b > 0.0 { dot / (mag_a * mag_b) } else { 0.0 } } + + fn extract_vector(val: &serde_json::Value) -> Option> { + val.get("vector") + .and_then(|v| serde_json::from_value(v.clone()).ok()) + } + + fn extract_metadata(val: &serde_json::Value) -> serde_json::Value { + val.get("metadata").cloned().unwrap_or(serde_json::Value::Null) + } + + fn extract_key(val: &serde_json::Value) -> String { + val.get("key") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string() + } } #[async_trait] @@ -99,20 +123,17 @@ impl VectorStore for SurrealDbStore { actual: embedding.len(), }); } - - let record = EmbeddingRecord { - id: None, - vector: embedding.clone(), - metadata, - }; - - let _: Option = self + let payload = serde_json::json!({ + "key": id, + "vector": embedding, + "metadata": metadata, + }); + let _: Option = self .db .update((self.table.as_str(), id)) - .content(record) + .content(payload) .await - .map_err(|e| EmbeddingError::StoreError(format!("Upsert failed: {}", e)))?; - + .map_err(|e| EmbeddingError::StoreError(format!("Upsert failed: {e}")))?; Ok(()) } @@ -128,31 +149,32 @@ impl VectorStore for SurrealDbStore { actual: embedding.len(), }); } - - let all_records: Vec = self + let all_records: Vec = self .db .select(&self.table) .await - .map_err(|e| EmbeddingError::StoreError(format!("Search failed: {}", e)))?; + .map_err(|e| EmbeddingError::StoreError(format!("Search failed: {e}")))?; - let mut scored_results: Vec<(String, f32, serde_json::Value)> = all_records + let mut scored: Vec<(String, f32, serde_json::Value)> = all_records .into_iter() .filter_map(|record| { - let id = record.id?.id.to_string(); - let score = self.compute_cosine_similarity(embedding, &record.vector); - Some((id, score, record.metadata)) + let id = Self::extract_key(&record); + let vector = Self::extract_vector(&record)?; + let metadata = Self::extract_metadata(&record); + let score = self.compute_cosine_similarity(embedding, &vector); + Some((id, score, metadata)) }) .collect(); - scored_results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); if let Some(f) = filter { if let Some(min_score) = f.min_score { - scored_results.retain(|(_, score, _)| *score >= min_score); + scored.retain(|(_, score, _)| *score >= min_score); } } - let results = scored_results + Ok(scored .into_iter() .take(limit) .map(|(id, score, metadata)| SearchResult { @@ -161,33 +183,34 @@ impl VectorStore for SurrealDbStore { embedding: None, metadata, }) - .collect(); - - Ok(results) + .collect()) } async fn get(&self, id: &str) -> Result, EmbeddingError> { - let record: Option = self + let record: Option = self .db .select((self.table.as_str(), id)) .await - .map_err(|e| EmbeddingError::StoreError(format!("Get failed: {}", e)))?; + .map_err(|e| EmbeddingError::StoreError(format!("Get failed: {e}")))?; - Ok(record.map(|r| SearchResult { - id: id.to_string(), - score: 1.0, - embedding: Some(r.vector), - metadata: r.metadata, + Ok(record.map(|r| { + let vector = Self::extract_vector(&r); + let metadata = Self::extract_metadata(&r); + SearchResult { + id: id.to_string(), + score: 1.0, + embedding: vector, + metadata, + } })) } async fn delete(&self, id: &str) -> Result { - let result: Option = self + let result: Option = self .db .delete((self.table.as_str(), id)) .await - .map_err(|e| EmbeddingError::StoreError(format!("Delete failed: {}", e)))?; - + .map_err(|e| EmbeddingError::StoreError(format!("Delete failed: {e}")))?; Ok(result.is_some()) } @@ -196,12 +219,11 @@ impl VectorStore for SurrealDbStore { } async fn count(&self) -> Result { - let records: Vec = self + let records: Vec = self .db .select(&self.table) .await - .map_err(|e| EmbeddingError::StoreError(format!("Count failed: {}", e)))?; - + .map_err(|e| EmbeddingError::StoreError(format!("Count failed: {e}")))?; Ok(records.len()) } } @@ -253,11 +275,10 @@ mod tests { vec![0.0, 1.0, 0.0], vec![0.5, 0.5, 0.0], ]; - for (i, embedding) in embeddings.iter().enumerate() { store .upsert( - &format!("id_{}", i), + &format!("id_{i}"), embedding, serde_json::json!({"idx": i}), ) @@ -266,10 +287,7 @@ mod tests { } let query = vec![1.0, 0.0, 0.0]; - let results = store - .search(&query, 2, None) - .await - .expect("Failed to search"); + let results = store.search(&query, 2, None).await.expect("Failed to search"); assert_eq!(results.len(), 2); assert_eq!(results[0].id, "id_0"); diff --git a/crates/stratum-graph/Cargo.toml b/crates/stratum-graph/Cargo.toml new file mode 100644 index 0000000..e0bab3d --- /dev/null +++ b/crates/stratum-graph/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "stratum-graph" +version = "0.1.0" +edition.workspace = true +description = "Knowledge domain: ActionGraph, ActionNode, Capability types and graph algorithms" +license.workspace = true + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +async-trait = { workspace = true } +tokio = { workspace = true } +uuid = { workspace = true } + +surrealdb = { workspace = true, optional = true } + +[features] +default = [] +mem-store = ["surrealdb", "surrealdb/kv-mem"] +surrealkv-store = ["surrealdb", "surrealdb/kv-surrealkv"] + +[dev-dependencies] +tokio-test = { workspace = true } +tempfile = { workspace = true } diff --git a/crates/stratum-graph/src/graph.rs b/crates/stratum-graph/src/graph.rs new file mode 100644 index 0000000..a3bee10 --- /dev/null +++ b/crates/stratum-graph/src/graph.rs @@ -0,0 +1,402 @@ +use std::collections::{HashMap, HashSet}; + +use anyhow::{anyhow, Result}; + +use crate::types::{ActionNode, Capability, NodeId}; + +#[derive(Debug, Default)] +pub struct ActionGraph { + nodes: HashMap, + /// capability → set of nodes that produce it + producers: HashMap>, + /// capability → set of nodes that consume it + consumers: HashMap>, + /// (NATS subject pattern, entry NodeId) + trigger_index: Vec<(String, NodeId)>, +} + +impl ActionGraph { + /// Build an ActionGraph from a set of already-parsed nodes. + pub fn from_nodes(nodes: Vec) -> Result { + let mut g = Self::default(); + for node in nodes { + g.insert_node(node); + } + g.check_cycles()?; + Ok(g) + } + + fn insert_node(&mut self, node: ActionNode) { + for cap in node.output_schemas.keys() { + self.producers + .entry(cap.clone()) + .or_default() + .insert(node.id.clone()); + } + for cap in node.input_schemas.keys() { + self.consumers + .entry(cap.clone()) + .or_default() + .insert(node.id.clone()); + } + for pattern in &node.triggers { + self.trigger_index.push((pattern.clone(), node.id.clone())); + } + self.nodes.insert(node.id.clone(), node); + } + + fn remove_node(&mut self, id: &NodeId) -> Option { + let node = self.nodes.remove(id)?; + for cap in node.output_schemas.keys() { + if let Some(set) = self.producers.get_mut(cap) { + set.remove(id); + } + } + for cap in node.input_schemas.keys() { + if let Some(set) = self.consumers.get_mut(cap) { + set.remove(id); + } + } + self.trigger_index.retain(|(_, nid)| nid != id); + Some(node) + } + + /// Apply a hot-reload update for a single file. + /// `new_node = None` means the file was deleted. + /// Returns the NodeIds that changed. + pub fn apply_update(&mut self, new_node: Option) -> Result> { + let changed_id = match &new_node { + Some(n) => n.id.clone(), + None => return Ok(vec![]), + }; + self.remove_node(&changed_id); + if let Some(node) = new_node { + self.insert_node(node); + } + self.check_cycles()?; + Ok(vec![changed_id]) + } + + /// Return nodes currently in the graph — used for observability. + pub fn nodes(&self) -> &HashMap { + &self.nodes + } + + /// Topological plan from entry nodes matched by NATS subject pattern. + /// Returns `Vec>` where each inner Vec is a parallel stage. + pub fn plan(&self, subject: &str, _payload: &serde_json::Value) -> Result>> { + let roots: Vec = self + .trigger_index + .iter() + .filter(|(pattern, _)| subject_matches(pattern, subject)) + .map(|(_, id)| id.clone()) + .collect(); + + if roots.is_empty() { + return Err(anyhow!("no entry nodes match NATS subject '{subject}'")); + } + + self.toposort_from(&roots) + } + + fn toposort_from(&self, roots: &[NodeId]) -> Result>> { + // Collect all reachable nodes (BFS) to build a subgraph + let mut reachable: HashSet = HashSet::new(); + let mut queue: Vec = roots.to_vec(); + while let Some(id) = queue.pop() { + if !reachable.insert(id.clone()) { + continue; + } + let caps: Vec<_> = self + .nodes + .get(&id) + .map(|n| n.output_schemas.keys().cloned().collect()) + .unwrap_or_default(); + for cap in caps { + let downstream: Vec<_> = self + .consumers + .get(&cap) + .map(|c| c.iter().cloned().collect()) + .unwrap_or_default(); + queue.extend(downstream.into_iter().filter(|c| !reachable.contains(c))); + } + } + + // Assign depth via longest-path in DAG + let mut memo: HashMap = HashMap::new(); + for id in &reachable { + self.node_depth(id, &mut memo); + } + + // Group by depth + let max_depth = memo.values().copied().max().unwrap_or(0); + let mut stages: Vec> = vec![vec![]; max_depth + 1]; + for id in &reachable { + let depth = memo[id]; + stages[depth].push(id.clone()); + } + + // Remove empty stages (shouldn't happen but be defensive) + stages.retain(|s| !s.is_empty()); + + Ok(stages) + } + + fn node_depth(&self, id: &NodeId, memo: &mut HashMap) -> usize { + if let Some(&d) = memo.get(id) { + return d; + } + let node = match self.nodes.get(id) { + Some(n) => n, + None => { + memo.insert(id.clone(), 0); + return 0; + } + }; + + // Depth = 1 + max depth of all nodes that produce our required capabilities + let mut max_dep_depth = 0usize; + for cap in node.input_schemas.keys() { + if let Some(producers) = self.producers.get(cap) { + for prod_id in producers { + let d = self.node_depth(prod_id, memo); + max_dep_depth = max_dep_depth.max(d + 1); + } + } + } + + memo.insert(id.clone(), max_dep_depth); + max_dep_depth + } + + fn check_cycles(&self) -> Result<()> { + // DFS cycle detection + let mut visited: HashSet = HashSet::new(); + let mut stack: HashSet = HashSet::new(); + + for id in self.nodes.keys() { + if !visited.contains(id) { + self.dfs_cycle(id, &mut visited, &mut stack)?; + } + } + Ok(()) + } + + fn dfs_cycle( + &self, + id: &NodeId, + visited: &mut HashSet, + stack: &mut HashSet, + ) -> Result<()> { + visited.insert(id.clone()); + stack.insert(id.clone()); + + let caps: Vec<_> = self + .nodes + .get(id) + .map(|n| n.output_schemas.keys().cloned().collect()) + .unwrap_or_default(); + for cap in caps { + let downstream: Vec<_> = self + .consumers + .get(&cap) + .map(|c| c.iter().cloned().collect()) + .unwrap_or_default(); + for next in &downstream { + if stack.contains(next) { + return Err(anyhow!( + "cycle detected involving nodes '{id}' and '{next}'" + )); + } + if !visited.contains(next) { + self.dfs_cycle(next, visited, stack)?; + } + } + } + + stack.remove(id); + Ok(()) + } +} + +/// Match a NATS subject against a pattern supporting `*` (single token) and `>` (multi-token). +fn subject_matches(pattern: &str, subject: &str) -> bool { + let p_parts: Vec<&str> = pattern.split('.').collect(); + let s_parts: Vec<&str> = subject.split('.').collect(); + + let mut pi = 0; + let mut si = 0; + + while pi < p_parts.len() && si < s_parts.len() { + match p_parts[pi] { + ">" => return true, + "*" => { + pi += 1; + si += 1; + } + tok => { + if tok != s_parts[si] { + return false; + } + pi += 1; + si += 1; + } + } + } + + pi == p_parts.len() && si == s_parts.len() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{BackoffStrategy, RetryPolicy}; + use std::path::PathBuf; + + fn retry() -> RetryPolicy { + RetryPolicy { + max: 3, + backoff_secs: 5, + strategy: BackoffStrategy::Exponential, + } + } + + fn cap(s: &str) -> Capability { + Capability(s.to_owned()) + } + + fn cap_schema(s: &str) -> (Capability, PathBuf) { + ( + Capability(s.to_owned()), + PathBuf::from(format!("schemas/{s}.ncl")), + ) + } + + fn node(id: &str, inputs: &[&str], outputs: &[&str], triggers: &[&str]) -> ActionNode { + ActionNode { + id: NodeId(id.to_owned()), + handler: PathBuf::from(format!("scripts/nu/{id}.nu")), + input_schemas: inputs.iter().map(|c| cap_schema(c)).collect(), + output_schemas: outputs.iter().map(|c| cap_schema(c)).collect(), + compensate: None, + retry: retry(), + timeout_secs: 120, + atomic: true, + triggers: triggers.iter().map(|s| s.to_string()).collect(), + } + } + + #[test] + fn test_toposort_linear() { + let nodes = vec![ + node("lint", &[], &["linted"], &["dev.crate.*.modified"]), + node("build", &["linted"], &["built"], &[]), + node("install", &["built"], &["installed"], &[]), + ]; + let g = ActionGraph::from_nodes(nodes).unwrap(); + let stages = g + .plan("dev.crate.foo.modified", &serde_json::Value::Null) + .unwrap(); + + assert_eq!(stages.len(), 3); + assert_eq!(stages[0], vec![NodeId("lint".into())]); + assert_eq!(stages[1], vec![NodeId("build".into())]); + assert_eq!(stages[2], vec![NodeId("install".into())]); + } + + #[test] + fn test_toposort_parallel() { + // lint and fmt are both stage-0 (no inputs), build depends on both + let nodes = vec![ + node("lint", &[], &["linted"], &["dev.crate.*.modified"]), + node("fmt", &[], &["formatted"], &["dev.crate.*.modified"]), + node("build", &["linted", "formatted"], &["built"], &[]), + ]; + let g = ActionGraph::from_nodes(nodes).unwrap(); + let stages = g + .plan("dev.crate.foo.modified", &serde_json::Value::Null) + .unwrap(); + + // stage[0] must have both lint and fmt (order may vary) + let mut stage0 = stages[0].clone(); + stage0.sort_by(|a, b| a.0.cmp(&b.0)); + assert_eq!(stage0, vec![NodeId("fmt".into()), NodeId("lint".into())]); + assert_eq!(stages[1], vec![NodeId("build".into())]); + } + + #[test] + fn test_cycle_detection() { + // a → b → a (cycle via capabilities) + let nodes = vec![ + node("a", &["cap-b"], &["cap-a"], &["trigger"]), + node("b", &["cap-a"], &["cap-b"], &[]), + ]; + let err = ActionGraph::from_nodes(nodes).unwrap_err(); + assert!(err.to_string().contains("cycle")); + } + + #[test] + fn test_plan_by_subject() { + let nodes = vec![ + node("lint", &[], &["linted"], &["dev.crate.*.modified"]), + node("deploy", &[], &["deployed"], &["prod.deploy.requested"]), + ]; + let g = ActionGraph::from_nodes(nodes).unwrap(); + + let stages = g + .plan("dev.crate.my-crate.modified", &serde_json::Value::Null) + .unwrap(); + assert_eq!(stages[0], vec![NodeId("lint".into())]); + + let err = g + .plan("prod.deploy.requested", &serde_json::Value::Null) + .unwrap(); + assert_eq!(err[0], vec![NodeId("deploy".into())]); + } + + #[test] + fn test_apply_update_hot_reload() { + let nodes = vec![node("lint", &[], &["linted"], &["dev.*"])]; + let mut g = ActionGraph::from_nodes(nodes).unwrap(); + + // Update lint to also produce "formatted" + let updated = ActionNode { + id: NodeId("lint".into()), + handler: PathBuf::from("scripts/nu/lint.nu"), + input_schemas: HashMap::new(), + output_schemas: [cap_schema("linted"), cap_schema("formatted")] + .into_iter() + .collect(), + compensate: None, + retry: retry(), + timeout_secs: 120, + atomic: true, + triggers: vec!["dev.*".to_string()], + }; + + let changed = g.apply_update(Some(updated)).unwrap(); + assert_eq!(changed, vec![NodeId("lint".into())]); + + // Verify the graph reflects the update + let n = g.nodes().get(&NodeId("lint".into())).unwrap(); + assert!(n.output_schemas.contains_key(&cap("formatted"))); + } + + #[test] + fn test_subject_wildcard_star() { + assert!(subject_matches( + "dev.crate.*.modified", + "dev.crate.foo.modified" + )); + assert!(!subject_matches( + "dev.crate.*.modified", + "dev.crate.foo.bar.modified" + )); + } + + #[test] + fn test_subject_wildcard_gt() { + assert!(subject_matches("dev.>", "dev.crate.foo.modified")); + assert!(subject_matches("dev.>", "dev.x")); + } +} diff --git a/crates/stratum-graph/src/lib.rs b/crates/stratum-graph/src/lib.rs new file mode 100644 index 0000000..d3e86c1 --- /dev/null +++ b/crates/stratum-graph/src/lib.rs @@ -0,0 +1,9 @@ +pub mod graph; +pub mod repository; +pub mod types; + +pub use graph::ActionGraph; +pub use repository::{GraphRepository, InMemoryGraphRepository}; +#[cfg(any(feature = "mem-store", feature = "surrealkv-store"))] +pub use repository::SurrealGraphRepository; +pub use types::{ActionNode, BackoffStrategy, Capability, NodeId, RetryPolicy}; diff --git a/crates/stratum-graph/src/repository.rs b/crates/stratum-graph/src/repository.rs new file mode 100644 index 0000000..f0cc2bd --- /dev/null +++ b/crates/stratum-graph/src/repository.rs @@ -0,0 +1,151 @@ +use anyhow::Result; +use async_trait::async_trait; +use std::collections::HashMap; +use tokio::sync::RwLock; + +use crate::types::{ActionNode, NodeId}; + +#[cfg(any(feature = "mem-store", feature = "surrealkv-store"))] +pub use surreal_repository::SurrealGraphRepository; + +#[cfg(any(feature = "mem-store", feature = "surrealkv-store"))] +mod surreal_repository { + use anyhow::{Context, Result}; + use async_trait::async_trait; + use serde::Serialize; + #[cfg(feature = "mem-store")] + use surrealdb::engine::local::Mem; + use surrealdb::{engine::local::Db, Surreal}; + + use crate::types::{ActionNode, NodeId}; + + use super::GraphRepository; + + fn to_json(v: &T) -> Result { + serde_json::to_value(v).context("serializing to serde_json::Value") + } + + fn from_json(v: serde_json::Value) -> Result { + serde_json::from_value(v).context("deserializing from serde_json::Value") + } + + pub struct SurrealGraphRepository { + db: Surreal, + } + + impl SurrealGraphRepository { + #[cfg(feature = "mem-store")] + pub async fn new_mem() -> Result { + let db = Surreal::new::(()) + .await + .context("creating in-memory SurrealDB for graph")?; + db.use_ns("stratum").use_db("graph").await?; + Ok(Self { db }) + } + + #[cfg(feature = "surrealkv-store")] + pub async fn new_surrealkv(path: impl AsRef) -> Result { + use surrealdb::engine::local::SurrealKv; + let db = Surreal::new::(path.as_ref()) + .await + .context("creating SurrealKV SurrealDB for graph")?; + db.use_ns("stratum").use_db("graph").await?; + Ok(Self { db }) + } + } + + #[async_trait] + impl GraphRepository for SurrealGraphRepository { + async fn upsert_node(&self, node: &ActionNode) -> Result<()> { + let id_str = node.id.0.clone(); + let payload = to_json(node)?; + let _: Option = self + .db + .upsert(("action_node_v1", id_str)) + .content(payload) + .await + .context("upsert_node")?; + Ok(()) + } + + async fn get_node(&self, id: &NodeId) -> Result> { + let raw: Option = self + .db + .select(("action_node_v1", id.0.clone())) + .await + .context("get_node")?; + raw.map(from_json).transpose() + } + + async fn list_nodes(&self) -> Result> { + let raw: Vec = self + .db + .select("action_node_v1") + .await + .context("list_nodes")?; + raw.into_iter().map(from_json).collect() + } + + async fn delete_node(&self, id: &NodeId) -> Result<()> { + let _: Option = self + .db + .delete(("action_node_v1", id.0.clone())) + .await + .context("delete_node")?; + Ok(()) + } + } +} + +/// Read-only observability view over the in-memory graph. +/// Write persistence (pipeline runs, capabilities) belongs to stratum-state. +#[async_trait] +pub trait GraphRepository: Send + Sync { + async fn upsert_node(&self, node: &ActionNode) -> Result<()>; + async fn get_node(&self, id: &NodeId) -> Result>; + async fn list_nodes(&self) -> Result>; + async fn delete_node(&self, id: &NodeId) -> Result<()>; +} + +/// In-memory implementation — runtime observability snapshot. +pub struct InMemoryGraphRepository { + store: RwLock>, +} + +impl InMemoryGraphRepository { + pub fn new() -> Self { + Self { + store: RwLock::new(HashMap::new()), + } + } +} + +impl Default for InMemoryGraphRepository { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl GraphRepository for InMemoryGraphRepository { + async fn upsert_node(&self, node: &ActionNode) -> Result<()> { + self.store + .write() + .await + .insert(node.id.clone(), node.clone()); + Ok(()) + } + + async fn get_node(&self, id: &NodeId) -> Result> { + Ok(self.store.read().await.get(id).cloned()) + } + + async fn list_nodes(&self) -> Result> { + Ok(self.store.read().await.values().cloned().collect()) + } + + async fn delete_node(&self, id: &NodeId) -> Result<()> { + self.store.write().await.remove(id); + Ok(()) + } +} diff --git a/crates/stratum-graph/src/types.rs b/crates/stratum-graph/src/types.rs new file mode 100644 index 0000000..0215f67 --- /dev/null +++ b/crates/stratum-graph/src/types.rs @@ -0,0 +1,64 @@ +use std::{collections::HashMap, path::PathBuf}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct NodeId(pub String); + +impl std::fmt::Display for NodeId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Capability(pub String); + +impl std::fmt::Display for Capability { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ActionNode { + pub id: NodeId, + /// Path to handler script relative to action-nodes/ + pub handler: PathBuf, + /// Capabilities this node requires as inputs + pub input_schemas: HashMap, + /// Capabilities this node produces as outputs + pub output_schemas: HashMap, + /// Optional compensation script for Saga rollback + pub compensate: Option, + pub retry: RetryPolicy, + pub timeout_secs: u64, + pub atomic: bool, + /// NATS subject patterns that trigger this node as an entry point + pub triggers: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RetryPolicy { + pub max: u32, + pub backoff_secs: u64, + pub strategy: BackoffStrategy, +} + +impl Default for RetryPolicy { + fn default() -> Self { + Self { + max: 3, + backoff_secs: 10, + strategy: BackoffStrategy::Exponential, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BackoffStrategy { + Fixed, + Linear, + Exponential, +} diff --git a/crates/stratum-llm/src/chain/circuit_breaker.rs b/crates/stratum-llm/src/chain/circuit_breaker.rs index 537fac6..59fa990 100644 --- a/crates/stratum-llm/src/chain/circuit_breaker.rs +++ b/crates/stratum-llm/src/chain/circuit_breaker.rs @@ -62,7 +62,7 @@ impl CircuitBreaker { match self.state() { CircuitState::Closed => true, CircuitState::Open => { - if let Some(last_failure) = *self.last_failure_time.read().unwrap() { + if let Some(last_failure) = *self.last_failure_time.read().unwrap_or_else(|e| e.into_inner()) { if last_failure.elapsed() >= self.config.reset_timeout { self.state .store(CircuitState::HalfOpen as u8, Ordering::SeqCst); @@ -77,7 +77,7 @@ impl CircuitBreaker { } pub fn record_success(&self) { - *self.last_success_time.write().unwrap() = Some(Instant::now()); + *self.last_success_time.write().unwrap_or_else(|e| e.into_inner()) = Some(Instant::now()); self.failure_count.store(0, Ordering::SeqCst); if self.state() == CircuitState::HalfOpen { @@ -91,7 +91,7 @@ impl CircuitBreaker { } pub fn record_failure(&self) { - *self.last_failure_time.write().unwrap() = Some(Instant::now()); + *self.last_failure_time.write().unwrap_or_else(|e| e.into_inner()) = Some(Instant::now()); match self.state() { CircuitState::Closed => { diff --git a/crates/stratum-orchestrator/Cargo.toml b/crates/stratum-orchestrator/Cargo.toml new file mode 100644 index 0000000..dbc9a7e --- /dev/null +++ b/crates/stratum-orchestrator/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "stratum-orchestrator" +version = "0.1.0" +edition.workspace = true +description = "Graph-driven workflow orchestrator with Saga compensation" +license.workspace = true + +[[bin]] +name = "orchestrator" +path = "src/bin/orchestrator.rs" + +[dependencies] +stratum-graph = { path = "../stratum-graph" } +stratum-state = { path = "../stratum-state" } +platform-nats = { path = "../platform-nats" } + +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +tokio = { workspace = true } +tokio-util = { workspace = true } +async-trait = { workspace = true } +uuid = { workspace = true } +chrono = { workspace = true, features = ["serde"] } +dashmap = { workspace = true } +notify = { workspace = true } +cedar-policy = { workspace = true } +regex = { workspace = true } +axum = { workspace = true } +tower-http = { workspace = true } +reqwest = { workspace = true } +bytes = { workspace = true } + +[dev-dependencies] +tokio-test = { workspace = true } +tempfile = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/stratum-orchestrator/src/auth/cedar.rs b/crates/stratum-orchestrator/src/auth/cedar.rs new file mode 100644 index 0000000..f0e2b39 --- /dev/null +++ b/crates/stratum-orchestrator/src/auth/cedar.rs @@ -0,0 +1,134 @@ +use std::path::Path; + +use anyhow::{anyhow, Context, Result}; +use cedar_policy::{Authorizer, Context as CedarContext, Entities, PolicySet, Request}; +use tracing::debug; + +pub struct CedarAuthorizer { + policy_set: PolicySet, + authorizer: Authorizer, +} + +impl CedarAuthorizer { + /// Load all .cedar policy files from a directory. + pub fn load_from_dir(dir: &Path) -> Result { + let entries = std::fs::read_dir(dir) + .with_context(|| format!("reading Cedar policy dir '{}'", dir.display()))?; + + let mut combined = String::new(); + + for entry in entries { + let entry = entry.context("reading Cedar dir entry")?; + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) != Some("cedar") { + continue; + } + let content = std::fs::read_to_string(&path) + .with_context(|| format!("reading Cedar policy '{}'", path.display()))?; + combined.push_str(&content); + combined.push('\n'); + } + + if combined.trim().is_empty() { + return Err(anyhow!( + "no .cedar policy files found in '{}'", + dir.display() + )); + } + + let policy_set: PolicySet = combined + .parse() + .map_err(|e| anyhow!("parsing Cedar policies from '{}': {e}", dir.display()))?; + + Ok(Self { + policy_set, + authorizer: Authorizer::new(), + }) + } + + /// Authorize an action. Returns Ok(()) if permitted, Err if denied. + pub fn authorize(&self, principal: &str, action: &str, resource: &str) -> Result<()> { + let principal_entity: cedar_policy::EntityUid = format!("User::\"{principal}\"") + .parse() + .with_context(|| format!("parsing principal EntityUid: User::\"{principal}\""))?; + + let action_entity: cedar_policy::EntityUid = format!("Action::\"{action}\"") + .parse() + .with_context(|| format!("parsing action EntityUid: Action::\"{action}\""))?; + + let resource_entity: cedar_policy::EntityUid = resource + .parse() + .with_context(|| format!("parsing resource EntityUid: {resource}"))?; + + let request = Request::new( + principal_entity, + action_entity, + resource_entity, + CedarContext::empty(), + None, + ) + .map_err(|e| anyhow!("building Cedar request: {e}"))?; + + let response = + self.authorizer + .is_authorized(&request, &self.policy_set, &Entities::empty()); + + debug!( + "Cedar: {principal} {action} {resource} → {:?}", + response.decision() + ); + + match response.decision() { + cedar_policy::Decision::Allow => Ok(()), + cedar_policy::Decision::Deny => Err(anyhow!( + "Cedar denied: principal '{principal}' action '{action}' resource '{resource}'" + )), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::TempDir; + + fn write_policy(dir: &TempDir, name: &str, content: &str) { + let path = dir.path().join(name); + let mut f = std::fs::File::create(path).unwrap(); + f.write_all(content.as_bytes()).unwrap(); + } + + const PERMIT_ORCHESTRATOR: &str = r#" +permit( + principal == User::"orchestrator", + action == Action::"execute", + resource == Node::"lint" +);"#; + + const FORBID_ALL: &str = r#" +forbid(principal, action, resource);"#; + + #[test] + fn test_permit_allows() { + let dir = TempDir::new().unwrap(); + write_policy(&dir, "permit.cedar", PERMIT_ORCHESTRATOR); + + let authz = CedarAuthorizer::load_from_dir(dir.path()).unwrap(); + authz + .authorize("orchestrator", "execute", "Node::\"lint\"") + .unwrap(); + } + + #[test] + fn test_deny_returns_err() { + let dir = TempDir::new().unwrap(); + write_policy(&dir, "forbid.cedar", FORBID_ALL); + + let authz = CedarAuthorizer::load_from_dir(dir.path()).unwrap(); + let err = authz + .authorize("orchestrator", "execute", "Node::\"lint\"") + .unwrap_err(); + assert!(err.to_string().contains("Cedar denied")); + } +} diff --git a/crates/stratum-orchestrator/src/auth/mod.rs b/crates/stratum-orchestrator/src/auth/mod.rs new file mode 100644 index 0000000..af1768f --- /dev/null +++ b/crates/stratum-orchestrator/src/auth/mod.rs @@ -0,0 +1,5 @@ +pub mod cedar; +pub mod vault; + +pub use cedar::CedarAuthorizer; +pub use vault::{NodeCredentials, VaultClient, VaultConfig, VaultHealth}; diff --git a/crates/stratum-orchestrator/src/auth/vault.rs b/crates/stratum-orchestrator/src/auth/vault.rs new file mode 100644 index 0000000..bed039b --- /dev/null +++ b/crates/stratum-orchestrator/src/auth/vault.rs @@ -0,0 +1,175 @@ +use std::collections::HashMap; + +use anyhow::{anyhow, Context, Result}; +use serde::{Deserialize, Serialize}; +use stratum_graph::types::NodeId; +use tracing::warn; + +#[derive(Debug, Clone)] +pub struct NodeCredentials { + /// Injected as child process environment variables + pub env_vars: HashMap, + /// Lease ID for revocation on failure/timeout + pub lease_id: String, +} + +#[derive(Debug, Clone)] +pub struct VaultConfig { + pub url: String, + pub token: String, +} + +pub struct VaultClient { + config: VaultConfig, + http: reqwest::Client, +} + +impl VaultClient { + pub fn new(config: VaultConfig) -> Self { + Self { + config, + http: reqwest::Client::new(), + } + } + + /// Fetch credentials for a node with the specified TTL. + /// Credentials are returned as env vars to inject into the child process. + pub async fn get_node_credentials( + &self, + node_id: &NodeId, + ttl_secs: u64, + ) -> Result { + let url = format!("{}/v1/auth/token/create", self.config.url); + + #[derive(Serialize)] + struct TokenCreateRequest { + ttl: String, + display_name: String, + renewable: bool, + } + + #[derive(Deserialize)] + struct TokenCreateResponse { + auth: TokenAuth, + } + + #[derive(Deserialize)] + struct TokenAuth { + client_token: String, + #[allow(dead_code)] + lease_duration: u64, + accessor: String, + } + + let body = TokenCreateRequest { + ttl: format!("{ttl_secs}s"), + display_name: format!("stratum-node-{}", node_id.0), + renewable: false, + }; + + let resp = self + .http + .post(&url) + .header("X-Vault-Token", &self.config.token) + .json(&body) + .send() + .await + .context("sending token create request to Vault")?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + return Err(anyhow!( + "Vault token create failed ({status}) for node '{}': {body}", + node_id.0 + )); + } + + let token_resp: TokenCreateResponse = + resp.json().await.context("parsing Vault response")?; + + let mut env_vars = HashMap::new(); + env_vars.insert("VAULT_TOKEN".to_string(), token_resp.auth.client_token); + env_vars.insert("VAULT_ADDR".to_string(), self.config.url.clone()); + + Ok(NodeCredentials { + env_vars, + lease_id: token_resp.auth.accessor, + }) + } + + /// Revoke a credential lease. Called on node failure to prevent dangling TTL tokens. + pub async fn revoke_credentials(&self, lease_id: &str) -> Result<()> { + let url = format!("{}/v1/auth/token/revoke-accessor", self.config.url); + + #[derive(Serialize)] + struct RevokeRequest<'a> { + accessor: &'a str, + } + + let resp = self + .http + .post(&url) + .header("X-Vault-Token", &self.config.token) + .json(&RevokeRequest { accessor: lease_id }) + .send() + .await + .with_context(|| format!("revoking Vault accessor '{lease_id}'"))?; + + if !resp.status().is_success() { + let status = resp.status(); + warn!("Vault revoke failed ({status}) for accessor '{lease_id}' — token will expire by TTL"); + } + + Ok(()) + } + + /// Health check — GET /v1/sys/health. + pub async fn health_check(&self) -> Result { + #[derive(Deserialize)] + struct VaultHealthResponse { + initialized: bool, + sealed: bool, + standby: bool, + } + + let url = format!("{}/v1/sys/health", self.config.url); + let resp = self + .http + .get(&url) + .header("X-Vault-Token", &self.config.token) + .send() + .await + .context("Vault health check")?; + + // Vault returns 200 (active), 429 (standby), 473 (performance standby), 501/503 + let status = resp.status(); + if status == reqwest::StatusCode::SERVICE_UNAVAILABLE { + return Ok(VaultHealth::Sealed); + } + + let body: VaultHealthResponse = resp.json().await.context("parsing Vault health")?; + + if !body.initialized { + return Ok(VaultHealth::Uninitialized); + } + if body.sealed { + return Ok(VaultHealth::Sealed); + } + if body.standby { + return Ok(VaultHealth::Standby); + } + + Ok(VaultHealth::Active) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum VaultHealth { + Active, + Standby, + Sealed, + Uninitialized, + Unreachable, +} diff --git a/crates/stratum-orchestrator/src/bin/orchestrator.rs b/crates/stratum-orchestrator/src/bin/orchestrator.rs new file mode 100644 index 0000000..3721ebe --- /dev/null +++ b/crates/stratum-orchestrator/src/bin/orchestrator.rs @@ -0,0 +1,358 @@ +use std::{path::PathBuf, sync::Arc, time::Duration}; + +use anyhow::{anyhow, Context, Result}; +use axum::{ + extract::{Path, State}, + response::IntoResponse, + routing::{get, post}, + Json, Router, +}; +use dashmap::DashMap; +use serde::{Deserialize, Serialize}; +use stratum_orchestrator::{ + auth::{CedarAuthorizer, VaultClient, VaultConfig, VaultHealth}, + context::PipelineContext, + executor::{AgentExecutor, NuExecutor}, + graph::{load_graph_from_dir, GraphWatcher}, + runner::StageRunner, +}; +use stratum_state::InMemoryStateTracker; +use tokio::sync::{RwLock, Semaphore}; +use tracing::{error, info, warn}; +use uuid::Uuid; + +// ─── Config ────────────────────────────────────────────────────────────────── + +#[derive(Debug, Deserialize)] +struct OrchestratorConfig { + #[allow(dead_code)] + surrealdb_url: String, + nats_url: String, + #[allow(dead_code)] + zot_url: String, + vault_url: String, + vault_token: Option, + action_nodes_dir: String, + schemas_dir: String, + cedar_policy_dir: String, + #[serde(default = "default_log_level")] + log_level: String, + #[serde(default = "default_http_port")] + http_port: u16, + #[serde(default)] + trusted_nkeys: Vec, + #[serde(default)] + features: Features, +} + +#[derive(Debug, Default, Deserialize)] +struct Features { + #[serde(default)] + nkey_auth_required: bool, + #[serde(default = "default_true")] + cedar_auth_required: bool, + #[serde(default = "default_true")] + #[allow(dead_code)] + schema_validation: bool, + #[serde(default)] + #[allow(dead_code)] + agent_executor_enabled: bool, +} + +fn default_log_level() -> String { + "info".to_string() +} +fn default_http_port() -> u16 { + 9088 +} +fn default_true() -> bool { + true +} + +impl OrchestratorConfig { + fn load(path: &std::path::Path) -> Result { + // Export from Nickel, parse JSON + let output = std::process::Command::new("nickel") + .arg("export") + .arg("--format") + .arg("json") + .arg(path) + .output() + .context("running nickel export on orchestrator config")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "orchestrator config failed Nickel typecheck:\n{}", + stderr.trim() + )); + } + + serde_json::from_slice(&output.stdout).context("parsing orchestrator config JSON") + } +} + +// ─── App State ─────────────────────────────────────────────────────────────── + +#[derive(Clone)] +struct AppState { + #[allow(dead_code)] + runner: Arc, + agent_exec: Arc, + graph: Arc>, + /// Per-target semaphore — Semaphore::new(1) enforces FIFO serialization per trigger subject + #[allow(dead_code)] + pipeline_locks: Arc>>, +} + +// ─── HTTP Handlers ──────────────────────────────────────────────────────────── + +#[derive(Serialize)] +struct HealthResponse { + status: &'static str, + node_count: usize, + vault: String, +} + +async fn health_handler(State(state): State) -> impl IntoResponse { + let node_count = state.graph.read().await.nodes().len(); + Json(HealthResponse { + status: "ok", + node_count, + vault: "connected".to_string(), + }) +} + +#[derive(Deserialize)] +struct AgentResult { + value: serde_json::Value, +} + +async fn agent_result_handler( + Path(correlation_id): Path, + State(state): State, + Json(body): Json, +) -> impl IntoResponse { + match state.agent_exec.resolve(correlation_id, body.value) { + Ok(()) => (axum::http::StatusCode::OK, "accepted"), + Err(e) => { + warn!("agent result error: {e}"); + (axum::http::StatusCode::NOT_FOUND, "no pending request") + } + } +} + +// ─── Pipeline Task ──────────────────────────────────────────────────────────── + +async fn run_with_semaphore( + sem: Arc, + runner: Arc, + ctx: Arc, + subject: String, +) { + match sem.acquire().await { + Ok(_permit) => { + info!(run_id = %ctx.run_id, %subject, "pipeline acquired lock, starting"); + if let Err(e) = runner.run_pipeline(ctx).await { + error!("pipeline error on '{subject}': {e}"); + } + } + Err(_) => { + error!("semaphore closed for subject '{subject}' — pipeline dropped"); + } + } +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +#[tokio::main] +async fn main() -> Result<()> { + // 1. Load config + let config_path = std::env::args() + .nth(2) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("config/orchestrator-config.ncl")); + + let cfg = OrchestratorConfig::load(&config_path)?; + + tracing_subscriber::fmt() + .with_env_filter(&cfg.log_level) + .init(); + + info!("orchestrator starting — config: {}", config_path.display()); + + // 2. State tracker (in-memory for now; SurrealDB in T0-B feature) + let state: Arc = Arc::new(InMemoryStateTracker::new()); + + // 3. Connect NATS + let nats_cfg = platform_nats::NatsConfig { + url: cfg.nats_url.clone(), + nkey_seed: None, + stream_name: "stratum-events".to_string(), + consumer_name: "orchestrator".to_string(), + subjects: vec!["dev.>".to_string(), "stratum.>".to_string()], + require_signed_messages: cfg.features.nkey_auth_required, + trusted_nkeys: cfg.trusted_nkeys.clone(), + }; + let event_stream = Arc::new( + platform_nats::EventStream::connect(&nats_cfg) + .await + .context("connecting to NATS")?, + ); + info!("connected to NATS at {}", cfg.nats_url); + + // 4. Load ActionGraph from action-nodes/ + let nodes_dir = PathBuf::from(&cfg.action_nodes_dir); + let initial_graph = load_graph_from_dir(&nodes_dir)?; + let graph = Arc::new(RwLock::new(initial_graph)); + info!( + "loaded {} nodes from '{}'", + graph.read().await.nodes().len(), + nodes_dir.display() + ); + + // 5. Start hot-reload watcher + let _watcher = GraphWatcher::new(nodes_dir.clone(), Arc::clone(&graph))?; + info!("watching '{}' for hot-reload", nodes_dir.display()); + + // 6. Initialize Cedar authorizer + let cedar = Arc::new( + CedarAuthorizer::load_from_dir(&PathBuf::from(&cfg.cedar_policy_dir)) + .context("loading Cedar policies")?, + ); + + // 7. Initialize Vault client + let vault_token = cfg + .vault_token + .clone() + .unwrap_or_else(|| std::env::var("VAULT_TOKEN").unwrap_or_default()); + let vault = Arc::new(VaultClient::new(VaultConfig { + url: cfg.vault_url.clone(), + token: vault_token, + })); + + // Health-check Vault + match vault.health_check().await { + Ok(VaultHealth::Active) => info!("Vault: active"), + Ok(health) => warn!("Vault: {:?}", health), + Err(e) => warn!("Vault health check failed (continuing): {e}"), + } + + // 8. Executors + let nu_exec = Arc::new(NuExecutor::new(PathBuf::from("."))); + let agent_exec = Arc::new(AgentExecutor::new( + format!("http://localhost:{}", cfg.http_port), + Arc::clone(&event_stream), + )); + + // 9. StageRunner + let runner = Arc::new(StageRunner { + graph: Arc::clone(&graph), + executor: nu_exec, + state: Arc::clone(&state), + auth: cedar, + vault, + cedar_required: cfg.features.cedar_auth_required, + }); + + let pipeline_locks: Arc>> = Arc::new(DashMap::new()); + + let app_state = AppState { + runner: Arc::clone(&runner), + agent_exec: Arc::clone(&agent_exec), + graph: Arc::clone(&graph), + pipeline_locks: Arc::clone(&pipeline_locks), + }; + + // 10. HTTP server (health + agent callback) + let app = Router::new() + .route("/health", get(health_handler)) + .route("/agent/result/:correlation_id", post(agent_result_handler)) + .with_state(app_state); + + let addr = format!("0.0.0.0:{}", cfg.http_port); + let listener = tokio::net::TcpListener::bind(&addr) + .await + .with_context(|| format!("binding HTTP server to {addr}"))?; + info!("HTTP server listening on {addr}"); + + tokio::spawn(async move { + axum::serve(listener, app).await.expect("HTTP server error"); + }); + + // 11. Graceful shutdown setup + let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); + + tokio::spawn(async move { + if let Ok(()) = tokio::signal::ctrl_c().await { + info!("SIGINT received — shutting down"); + let _ = shutdown_tx.send(true); + } + }); + + // 12. Event loop + info!("entering event loop"); + loop { + if *shutdown_rx.borrow() { + info!("shutdown signal received — draining in-flight pipelines"); + break; + } + + let messages = match event_stream.pull_batch(10).await { + Ok(m) => m, + Err(e) => { + error!("pull_batch error: {e}"); + tokio::time::sleep(Duration::from_secs(1)).await; + continue; + } + }; + + // Small yield to avoid busy-loop when no messages + if messages.is_empty() { + tokio::time::sleep(Duration::from_millis(100)).await; + continue; + } + + for (subject, payload, msg) in messages { + let trigger_payload: serde_json::Value = + serde_json::from_slice(&payload).unwrap_or(serde_json::Value::Null); + + let ctx = match PipelineContext::new( + subject.clone(), + trigger_payload, + Arc::clone(&state), + PathBuf::from(&cfg.schemas_dir), + ) + .await + { + Ok(c) => Arc::new(c), + Err(e) => { + error!("failed to create PipelineContext for '{subject}': {e}"); + let _ = msg.ack().await; + continue; + } + }; + + // Ack immediately after context creation — we own this message + if let Err(e) = msg.ack().await { + warn!("ack failed for '{subject}': {e}"); + } + + // Per-target semaphore: FIFO serialization within the same trigger subject. + // acquire() is called INSIDE the spawned task so tokio FIFO fairness applies. + let sem = pipeline_locks + .entry(subject.clone()) + .or_insert_with(|| Arc::new(Semaphore::new(1))) + .clone(); + + let runner = Arc::clone(&runner); + + tokio::spawn(run_with_semaphore(sem, runner, ctx, subject)); + } + } + + // Wait briefly for in-flight tasks (best-effort, max 30s) + tokio::time::sleep(Duration::from_secs(30)).await; + info!("orchestrator stopped"); + Ok(()) +} diff --git a/crates/stratum-orchestrator/src/context.rs b/crates/stratum-orchestrator/src/context.rs new file mode 100644 index 0000000..7621201 --- /dev/null +++ b/crates/stratum-orchestrator/src/context.rs @@ -0,0 +1,220 @@ +use std::{collections::HashMap, path::Path, sync::Arc}; + +use anyhow::{anyhow, Context, Result}; +use dashmap::DashMap; +use stratum_graph::types::Capability; +use stratum_state::{PipelineRun, PipelineRunId, StateTracker}; +use tracing::debug; + +pub struct PipelineContext { + pub run_id: PipelineRunId, + pub trigger_subject: String, + pub trigger_payload: serde_json::Value, + cache: DashMap, + state: Arc, + schema_dir: std::path::PathBuf, +} + +impl PipelineContext { + /// Creates a new context, writing the PipelineRun record to DB first. + pub async fn new( + subject: String, + payload: serde_json::Value, + state: Arc, + schema_dir: std::path::PathBuf, + ) -> Result { + let run = PipelineRun::new(subject.clone(), payload.clone()); + state + .create_run(&run) + .await + .context("creating pipeline run in DB")?; + + Ok(Self { + run_id: run.id, + trigger_subject: subject, + trigger_payload: payload, + cache: DashMap::new(), + state, + schema_dir, + }) + } + + /// Deposit a capability: DB write first, then schema validation, then cache update. + pub async fn deposit(&self, cap: &Capability, value: serde_json::Value) -> Result<()> { + // 1. Persist to DB first — crash-safe + self.state + .deposit_capability(&self.run_id, cap, &value) + .await + .with_context(|| format!("depositing capability '{cap}' to DB"))?; + + // 2. Schema validation (only if schema file exists) + let schema_path = self.schema_dir.join(format!("{}.ncl", cap.0)); + if schema_path.exists() { + validate_against_schema(&schema_path, &value) + .with_context(|| format!("schema validation failed for capability '{cap}'"))?; + } + + // 3. Update in-memory cache + self.cache.insert(cap.0.clone(), value); + debug!("deposited capability '{cap}'"); + + Ok(()) + } + + /// Extract a capability: cache first, DB fallback (crash recovery path). + pub async fn extract(&self, cap: &Capability) -> Result { + if let Some(v) = self.cache.get(&cap.0) { + return Ok(v.clone()); + } + + // DB fallback — covers the crash-and-restart case + self.state + .load_capability(&self.run_id, cap) + .await + .with_context(|| format!("loading capability '{cap}' from DB"))? + .ok_or_else(|| { + anyhow!( + "capability '{cap}' not found in pipeline run {}", + self.run_id + ) + }) + } + + /// Extract all required capabilities, returning a map. + pub async fn extract_inputs( + &self, + required: &[Capability], + ) -> Result> { + let mut result = HashMap::with_capacity(required.len()); + for cap in required { + let value = self + .extract(cap) + .await + .with_context(|| format!("extracting required input '{cap}'"))?; + result.insert(cap.clone(), value); + } + Ok(result) + } +} + +/// Validate a JSON value against a Nickel schema file. +/// Runs `nickel export --format json ` to get the JSON Schema, +/// then checks required fields are present. +/// +/// Only called when schema file exists; silently skipped otherwise. +fn validate_against_schema(schema_path: &Path, value: &serde_json::Value) -> Result<()> { + let output = std::process::Command::new("nickel") + .arg("export") + .arg("--format") + .arg("json") + .arg(schema_path) + .output() + .with_context(|| { + format!( + "running nickel export on schema '{}'", + schema_path.display() + ) + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "schema '{}' failed to export: {}", + schema_path.display(), + stderr.trim() + )); + } + + let schema: serde_json::Value = + serde_json::from_slice(&output.stdout).context("parsing schema JSON from nickel export")?; + + // Check required fields if the schema has a `required` array + if let Some(required) = schema.get("required").and_then(|r| r.as_array()) { + let obj = value.as_object().ok_or_else(|| { + anyhow!("capability value must be a JSON object when schema defines required fields") + })?; + for req in required { + if let Some(field) = req.as_str() { + if !obj.contains_key(field) { + return Err(anyhow!( + "required field '{field}' missing from capability value" + )); + } + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use stratum_state::InMemoryStateTracker; + use tempfile::TempDir; + + async fn make_ctx() -> (PipelineContext, TempDir) { + let dir = TempDir::new().unwrap(); + let state = Arc::new(InMemoryStateTracker::new()); + let ctx = PipelineContext::new( + "test.subject".to_string(), + serde_json::Value::Null, + state, + dir.path().to_path_buf(), + ) + .await + .unwrap(); + (ctx, dir) + } + + #[tokio::test] + async fn test_deposit_and_extract() { + let (ctx, _dir) = make_ctx().await; + let cap = Capability("linted".to_string()); + let value = serde_json::json!({"warnings": 0}); + + ctx.deposit(&cap, value.clone()).await.unwrap(); + let extracted = ctx.extract(&cap).await.unwrap(); + assert_eq!(extracted, value); + } + + #[tokio::test] + async fn test_extract_missing_returns_err() { + let (ctx, _dir) = make_ctx().await; + let cap = Capability("missing".to_string()); + let err = ctx.extract(&cap).await.unwrap_err(); + assert!(err.to_string().contains("missing")); + } + + #[tokio::test] + async fn test_db_fallback_on_cache_miss() { + let dir = TempDir::new().unwrap(); + let state = Arc::new(InMemoryStateTracker::new()); + + let ctx1 = PipelineContext::new( + "test".to_string(), + serde_json::Value::Null, + Arc::clone(&state) as Arc, + dir.path().to_path_buf(), + ) + .await + .unwrap(); + + let cap = Capability("linted".to_string()); + let value = serde_json::json!({"ok": true}); + ctx1.deposit(&cap, value.clone()).await.unwrap(); + + // Simulate restart: new context, same state — cache is empty + let ctx2 = PipelineContext { + run_id: ctx1.run_id.clone(), + trigger_subject: "test".to_string(), + trigger_payload: serde_json::Value::Null, + cache: DashMap::new(), + state: state as Arc, + schema_dir: dir.path().to_path_buf(), + }; + + let extracted = ctx2.extract(&cap).await.unwrap(); + assert_eq!(extracted, value); + } +} diff --git a/crates/stratum-orchestrator/src/executor/agent.rs b/crates/stratum-orchestrator/src/executor/agent.rs new file mode 100644 index 0000000..61c8b0f --- /dev/null +++ b/crates/stratum-orchestrator/src/executor/agent.rs @@ -0,0 +1,93 @@ +use std::{sync::Arc, time::Duration}; + +use anyhow::{anyhow, Result}; +use dashmap::DashMap; +use serde::{Deserialize, Serialize}; +use stratum_state::{PipelineRunId, PipelineStatus, StateTracker}; +use tokio::sync::oneshot; +use tracing::warn; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentRequest { + pub node_id: String, + pub run_id: String, + pub correlation_id: Uuid, + pub callback_url: String, + pub payload: serde_json::Value, +} + +pub struct AgentExecutor { + pending: DashMap>, + callback_url: String, + nats: Arc, +} + +impl AgentExecutor { + pub fn new(callback_url: String, nats: Arc) -> Self { + Self { + pending: DashMap::new(), + callback_url, + nats, + } + } + + /// Dispatch a request to an AI agent via NATS, then suspend the pipeline + /// until the agent POSTs its result to the callback endpoint. + pub async fn dispatch( + &self, + request_payload: serde_json::Value, + node_id: &str, + timeout_secs: u64, + state: &dyn StateTracker, + run_id: &PipelineRunId, + ) -> Result { + let correlation_id = Uuid::new_v4(); + let (tx, rx) = oneshot::channel(); + self.pending.insert(correlation_id, tx); + + state + .update_status(run_id, PipelineStatus::AwaitingAgent) + .await?; + + let req = AgentRequest { + node_id: node_id.to_string(), + run_id: run_id.to_string(), + correlation_id, + callback_url: format!("{}/agent/result/{}", self.callback_url, correlation_id), + payload: request_payload, + }; + + let subject = format!("stratum.agent.request.{correlation_id}"); + let payload = bytes::Bytes::from(serde_json::to_vec(&req)?); + + self.nats + .publish(&subject, payload) + .await + .map_err(|e| anyhow!("failed to publish agent request: {e}"))?; + + let result = tokio::time::timeout(Duration::from_secs(timeout_secs), rx) + .await + .map_err(|_| { + self.pending.remove(&correlation_id); + anyhow!("agent request {correlation_id} timed out after {timeout_secs}s") + })? + .map_err(|_| anyhow!("agent response channel closed unexpectedly"))?; + + state.update_status(run_id, PipelineStatus::Running).await?; + + Ok(result) + } + + /// Called by the HTTP handler when an agent POSTs its result. + pub fn resolve(&self, correlation_id: Uuid, value: serde_json::Value) -> Result<()> { + let (_, tx) = self.pending.remove(&correlation_id).ok_or_else(|| { + anyhow!("no pending agent request for correlation_id {correlation_id}") + })?; + + tx.send(value).map_err(|_| { + warn!("agent result for {correlation_id} arrived after pipeline timed out"); + anyhow!("pipeline for {correlation_id} already timed out — result discarded") + }) + } +} diff --git a/crates/stratum-orchestrator/src/executor/mod.rs b/crates/stratum-orchestrator/src/executor/mod.rs new file mode 100644 index 0000000..af63d08 --- /dev/null +++ b/crates/stratum-orchestrator/src/executor/mod.rs @@ -0,0 +1,7 @@ +pub mod agent; +pub mod nu; +pub mod sanitize; + +pub use agent::AgentExecutor; +pub use nu::NuExecutor; +pub use sanitize::redact_secrets; diff --git a/crates/stratum-orchestrator/src/executor/nu.rs b/crates/stratum-orchestrator/src/executor/nu.rs new file mode 100644 index 0000000..67d742c --- /dev/null +++ b/crates/stratum-orchestrator/src/executor/nu.rs @@ -0,0 +1,126 @@ +use std::{collections::HashMap, path::PathBuf, time::Duration}; + +use anyhow::{anyhow, Context, Result}; +use stratum_graph::types::Capability; +use tokio::io::AsyncWriteExt; +use tracing::{debug, warn}; + +use stratum_state::PipelineRunId; + +use crate::{ + auth::NodeCredentials, + executor::sanitize::{redact_secrets, sanitize_env}, +}; +use stratum_graph::ActionNode; + +pub struct NuExecutor { + scripts_base: PathBuf, +} + +impl NuExecutor { + pub fn new(scripts_base: PathBuf) -> Self { + Self { scripts_base } + } + + /// Execute a node's handler script. + /// + /// - Inputs are passed as JSON on stdin + /// - Outputs are read as JSON from stdout + /// - Credentials are injected as environment variables + /// - Timeout enforced; stderr secrets are redacted before returning errors + pub async fn run( + &self, + node: &ActionNode, + inputs: &HashMap, + creds: &NodeCredentials, + timeout_secs: u64, + ) -> Result> { + let script_path = self.scripts_base.join(&node.handler); + let input_json = serde_json::to_string(inputs).context("serializing inputs to JSON")?; + + let mut cmd = tokio::process::Command::new("nu"); + cmd.arg("--no-config-file") + .arg(&script_path) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .envs(sanitize_env(creds.env_vars.iter())); + + let mut child = cmd + .spawn() + .with_context(|| format!("spawning nu for node '{}'", node.id))?; + + if let Some(stdin) = child.stdin.take() { + let mut writer = tokio::io::BufWriter::new(stdin); + writer + .write_all(input_json.as_bytes()) + .await + .context("writing inputs to nu stdin")?; + writer.flush().await.context("flushing nu stdin")?; + } + + let output = + tokio::time::timeout(Duration::from_secs(timeout_secs), child.wait_with_output()) + .await + .map_err(|_| anyhow!("node '{}' timed out after {timeout_secs}s", node.id))??; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let redacted = redact_secrets(&stderr, &creds.env_vars); + return Err(anyhow!( + "node '{}' exited with {}: {redacted}", + node.id, + output.status + )); + } + + let raw: HashMap = serde_json::from_slice(&output.stdout) + .with_context(|| { + format!( + "node '{}' stdout is not valid JSON — handler must emit a JSON object", + node.id + ) + })?; + + debug!("node '{}' produced {} capabilities", node.id, raw.len()); + + Ok(raw.into_iter().map(|(k, v)| (Capability(k), v)).collect()) + } + + /// Execute a compensation (rollback) script. + /// Failures are logged at WARN but do not propagate — compensation is best-effort. + pub async fn run_compensation(&self, node: &ActionNode, run_id: &PipelineRunId) -> Result<()> { + let Some(comp_rel) = &node.compensate else { + return Ok(()); + }; + + let script_path = self.scripts_base.join(comp_rel); + let run_id_str = run_id.to_string(); + + let output = tokio::process::Command::new("nu") + .arg("--no-config-file") + .arg(&script_path) + .env("PIPELINE_RUN_ID", &run_id_str) + .output() + .await + .with_context(|| { + format!( + "spawning compensation script '{}' for node '{}'", + comp_rel.display(), + node.id + ) + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + warn!( + "compensation '{}' for node '{}' failed (non-fatal): {}", + comp_rel.display(), + node.id, + stderr.trim() + ); + } + + Ok(()) + } +} diff --git a/crates/stratum-orchestrator/src/executor/sanitize.rs b/crates/stratum-orchestrator/src/executor/sanitize.rs new file mode 100644 index 0000000..e9f6733 --- /dev/null +++ b/crates/stratum-orchestrator/src/executor/sanitize.rs @@ -0,0 +1,98 @@ +use std::{collections::HashMap, sync::LazyLock}; + +use regex::Regex; + +static SECRET_PATTERNS: LazyLock> = LazyLock::new(|| { + vec![ + // Generic key=value patterns + Regex::new(r"(?i)(password|secret|token|key|credential)\s*=\s*\S+").unwrap(), + // Vault token pattern (hvs.*) + Regex::new(r"hvs\.[A-Za-z0-9_\-]{20,}").unwrap(), + // AWS-style secret + Regex::new(r"(?i)aws.{0,10}secret.{0,10}=\s*\S+").unwrap(), + ] +}); + +/// Redact secrets from text before logging or returning in errors. +/// +/// 1. Replaces literal values of any env var with length >= 8 +/// 2. Replaces known secret patterns (key=value, Vault tokens) +pub fn redact_secrets(text: &str, env_values: &HashMap) -> String { + let mut result = text.to_owned(); + + // Redact any env var value that appears literally in output + for value in env_values.values() { + if value.len() >= 8 { + result = result.replace(value.as_str(), "[REDACTED]"); + } + } + + // Redact regex patterns + for re in SECRET_PATTERNS.iter() { + result = re + .replace_all(&result, |caps: ®ex::Captures| { + let full = &caps[0]; + // Keep everything up to and including '=' or the pattern prefix, + // replace the sensitive value portion + if let Some(eq_pos) = full.find('=') { + format!("{}=[REDACTED]", &full[..eq_pos]) + } else { + "[REDACTED]".to_owned() + } + }) + .to_string(); + } + + result +} + +/// Yield (key, value) pairs from env vars for use with Command::envs. +pub fn sanitize_env<'a>( + env: impl Iterator, +) -> impl Iterator { + env.map(|(k, v)| (k.as_str(), v.as_str())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_redact_env_var_values() { + let env = HashMap::from([ + ( + "API_TOKEN".to_string(), + "super-secret-token-12345".to_string(), + ), + ("SHORT".to_string(), "abc".to_string()), // too short, not redacted + ]); + let text = "Running with token=super-secret-token-12345 and SHORT=abc"; + let result = redact_secrets(text, &env); + assert!(!result.contains("super-secret-token-12345")); + assert!(result.contains("[REDACTED]")); + assert!(result.contains("SHORT=abc")); // short value not redacted + } + + #[test] + fn test_redact_vault_token() { + let text = "auth failed: hvs.CAESIBfgabcdefghijklmnopqrstuvwxyz0123456789"; + let result = redact_secrets(text, &HashMap::new()); + assert!(!result.contains("hvs.CAE")); + assert!(result.contains("[REDACTED]")); + } + + #[test] + fn test_redact_key_value_pattern() { + let text = "config: secret=mysupersecretvalue123"; + let result = redact_secrets(text, &HashMap::new()); + assert!(!result.contains("mysupersecretvalue123")); + assert!(result.contains("secret=[REDACTED]")); + } + + #[test] + fn test_clean_text_unchanged() { + let text = "cargo build succeeded: 0 errors, 0 warnings"; + let result = redact_secrets(text, &HashMap::new()); + assert_eq!(result, text); + } +} diff --git a/crates/stratum-orchestrator/src/graph/loader.rs b/crates/stratum-orchestrator/src/graph/loader.rs new file mode 100644 index 0000000..c72f1d2 --- /dev/null +++ b/crates/stratum-orchestrator/src/graph/loader.rs @@ -0,0 +1,66 @@ +use std::path::Path; + +use anyhow::{anyhow, Context, Result}; +use stratum_graph::{ActionGraph, ActionNode}; +use tracing::{debug, warn}; + +/// Load a single ActionNode from a .ncl file by running `nickel export --format json`. +pub fn load_node_from_ncl(path: &Path) -> Result { + let output = std::process::Command::new("nickel") + .arg("export") + .arg("--format") + .arg("json") + .arg(path) + .output() + .with_context(|| format!("running nickel export on '{}'", path.display()))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "nickel export failed on '{}': {}", + path.display(), + stderr.trim() + )); + } + + serde_json::from_slice::(&output.stdout).with_context(|| { + format!( + "parsing ActionNode JSON from '{}' — check that the .ncl file exports a valid NodeDefinition", + path.display() + ) + }) +} + +/// Load all .ncl files from `dir` and build an ActionGraph. +/// Fails fast on the first Nickel typecheck or parse error — no partial graphs. +pub fn load_graph_from_dir(dir: &Path) -> Result { + let entries = + std::fs::read_dir(dir).with_context(|| format!("reading dir '{}'", dir.display()))?; + + let mut nodes = Vec::new(); + + for entry in entries { + let entry = entry.context("reading dir entry")?; + let path = entry.path(); + + if path.extension().and_then(|e| e.to_str()) != Some("ncl") { + continue; + } + + debug!("loading node from '{}'", path.display()); + + match load_node_from_ncl(&path) { + Ok(node) => nodes.push(node), + Err(e) => { + return Err(e.context(format!("failed to load node from '{}'", path.display()))); + } + } + } + + if nodes.is_empty() { + warn!("no .ncl files found in '{}'", dir.display()); + } + + ActionGraph::from_nodes(nodes) + .with_context(|| format!("building ActionGraph from '{}'", dir.display())) +} diff --git a/crates/stratum-orchestrator/src/graph/mod.rs b/crates/stratum-orchestrator/src/graph/mod.rs new file mode 100644 index 0000000..2d61386 --- /dev/null +++ b/crates/stratum-orchestrator/src/graph/mod.rs @@ -0,0 +1,5 @@ +pub mod loader; +pub mod watcher; + +pub use loader::{load_graph_from_dir, load_node_from_ncl}; +pub use watcher::GraphWatcher; diff --git a/crates/stratum-orchestrator/src/graph/watcher.rs b/crates/stratum-orchestrator/src/graph/watcher.rs new file mode 100644 index 0000000..90b4548 --- /dev/null +++ b/crates/stratum-orchestrator/src/graph/watcher.rs @@ -0,0 +1,106 @@ +use std::{path::PathBuf, sync::Arc}; + +use anyhow::{Context, Result}; +use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; +use tokio::sync::{mpsc, RwLock}; +use tracing::{error, info, warn}; + +use stratum_graph::ActionGraph; + +use super::loader::load_node_from_ncl; + +pub struct GraphWatcher { + _watcher: RecommendedWatcher, +} + +impl GraphWatcher { + /// Spawn a file watcher on `dir`. On any .ncl file modification/creation/removal, + /// reload the affected node and update the shared graph. + pub fn new(dir: PathBuf, graph: Arc>) -> Result { + let (tx, mut rx) = mpsc::channel::>(64); + + let mut watcher = notify::recommended_watcher(move |event| { + let _ = tx.blocking_send(event); + }) + .context("creating filesystem watcher")?; + + watcher + .watch(&dir, RecursiveMode::NonRecursive) + .with_context(|| format!("watching '{}'", dir.display()))?; + + tokio::spawn(async move { + while let Some(event) = rx.recv().await { + match event { + Ok(e) => handle_event(e, &graph).await, + Err(e) => error!("watcher error: {e}"), + } + } + }); + + Ok(Self { _watcher: watcher }) + } +} + +async fn handle_event(event: Event, graph: &Arc>) { + let paths: Vec = event + .paths + .into_iter() + .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("ncl")) + .collect(); + + if paths.is_empty() { + return; + } + + match event.kind { + EventKind::Create(_) | EventKind::Modify(_) => { + for path in paths { + match load_node_from_ncl(&path) { + Ok(node) => { + let node_id = node.id.clone(); + let mut g = graph.write().await; + match g.apply_update(Some(node)) { + Ok(changed) => { + let changed_list = changed + .iter() + .map(|id| id.0.as_str()) + .collect::>() + .join(", "); + info!( + "hot-reload: updated node '{}' from '{}' (affected: [{}])", + node_id, + path.display(), + changed_list + ); + } + Err(e) => { + error!( + "hot-reload: failed to apply update from '{}': {e}", + path.display() + ); + } + } + } + Err(e) => { + warn!( + "hot-reload: failed to load '{}', keeping existing node: {e}", + path.display() + ); + } + } + } + } + EventKind::Remove(_) => { + // File removed — remove node from graph + for path in paths { + // We can't parse the deleted file; log and skip for now. + // The orchestrator operator must remove nodes intentionally. + warn!( + "hot-reload: file '{}' removed — node will remain in graph until restart", + path.display() + ); + } + } + _ => {} + } +} diff --git a/crates/stratum-orchestrator/src/lib.rs b/crates/stratum-orchestrator/src/lib.rs new file mode 100644 index 0000000..1e6a908 --- /dev/null +++ b/crates/stratum-orchestrator/src/lib.rs @@ -0,0 +1,5 @@ +pub mod auth; +pub mod context; +pub mod executor; +pub mod graph; +pub mod runner; diff --git a/crates/stratum-orchestrator/src/runner/mod.rs b/crates/stratum-orchestrator/src/runner/mod.rs new file mode 100644 index 0000000..981eb74 --- /dev/null +++ b/crates/stratum-orchestrator/src/runner/mod.rs @@ -0,0 +1,262 @@ +use std::{collections::HashMap, sync::Arc, time::Duration}; + +use anyhow::{anyhow, Result}; +use stratum_graph::{ActionGraph, ActionNode, Capability, NodeId}; +use stratum_state::{PipelineStatus, StateTracker, StepRecord}; +use tokio::{sync::RwLock, task::JoinSet}; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, warn}; + +use crate::{ + auth::{CedarAuthorizer, NodeCredentials, VaultClient}, + context::PipelineContext, + executor::NuExecutor, +}; +use stratum_graph::types::BackoffStrategy; + +pub struct StageRunner { + pub graph: Arc>, + pub executor: Arc, + pub state: Arc, + pub auth: Arc, + pub vault: Arc, + pub cedar_required: bool, +} + +impl StageRunner { + pub async fn run_pipeline(&self, ctx: Arc) -> Result { + let stages = { + let g = self.graph.read().await; + g.plan(&ctx.trigger_subject, &ctx.trigger_payload)? + }; + + info!( + run_id = %ctx.run_id, + stages = stages.len(), + "pipeline started" + ); + + let mut executed_stages: Vec> = Vec::new(); + let cancel = CancellationToken::new(); + + for (i, stage_nodes) in stages.iter().enumerate() { + info!(run_id = %ctx.run_id, stage = i, nodes = stage_nodes.len(), "executing stage"); + + match self.run_stage(stage_nodes, &ctx, cancel.clone()).await { + Ok(()) => { + executed_stages.push(stage_nodes.clone()); + } + Err(e) => { + cancel.cancel(); + error!(run_id = %ctx.run_id, stage = i, "stage failed: {e}"); + + self.state + .update_status(&ctx.run_id, PipelineStatus::Compensating) + .await?; + + self.compensate(&executed_stages, &ctx).await?; + + self.state + .update_status(&ctx.run_id, PipelineStatus::Compensated) + .await?; + + info!(run_id = %ctx.run_id, "pipeline compensated"); + return Ok(PipelineStatus::Compensated); + } + } + } + + self.state + .update_status(&ctx.run_id, PipelineStatus::Success) + .await?; + + info!(run_id = %ctx.run_id, "pipeline succeeded"); + Ok(PipelineStatus::Success) + } + + async fn run_stage( + &self, + nodes: &[NodeId], + ctx: &Arc, + cancel: CancellationToken, + ) -> Result<()> { + let graph = self.graph.read().await; + let mut set: JoinSet> = JoinSet::new(); + + for node_id in nodes { + let node = graph + .nodes() + .get(node_id) + .ok_or_else(|| anyhow!("node '{node_id}' not found in graph"))? + .clone(); + + let ctx = Arc::clone(ctx); + let exec = Arc::clone(&self.executor); + let auth = Arc::clone(&self.auth); + let vault = Arc::clone(&self.vault); + let state = Arc::clone(&self.state); + let tok = cancel.clone(); + let cedar_required = self.cedar_required; + + set.spawn(async move { + tokio::select! { + result = run_node(node, ctx, exec, auth, vault, state, cedar_required) => result, + _ = tok.cancelled() => Err(anyhow!("node cancelled by sibling failure")), + } + }); + } + + // Drop read lock before awaiting tasks + drop(graph); + + while let Some(res) = set.join_next().await { + match res.map_err(|e| anyhow!("join error: {e}"))? { + Ok(()) => {} + Err(e) => { + cancel.cancel(); + set.abort_all(); + return Err(e); + } + } + } + + Ok(()) + } + + /// Saga compensation: reverse order through executed stages, each stage parallel. + /// Errors in compensation are logged but never propagate — best-effort. + async fn compensate( + &self, + executed_stages: &[Vec], + ctx: &Arc, + ) -> Result<()> { + let graph = self.graph.read().await; + + for stage in executed_stages.iter().rev() { + let mut set: JoinSet> = JoinSet::new(); + + for node_id in stage { + let Some(node) = graph.nodes().get(node_id).cloned() else { + continue; + }; + if node.compensate.is_none() { + continue; + } + + let exec = Arc::clone(&self.executor); + let run_id = ctx.run_id.clone(); + + set.spawn(async move { exec.run_compensation(&node, &run_id).await }); + } + + while let Some(res) = set.join_next().await { + if let Err(e) = res.map_err(|e| anyhow!("join: {e}"))? { + error!("compensation error (non-fatal): {e}"); + } + } + } + + Ok(()) + } +} + +async fn run_node( + node: ActionNode, + ctx: Arc, + exec: Arc, + auth: Arc, + vault: Arc, + state: Arc, + cedar_required: bool, +) -> Result<()> { + // 1. Cedar authorization + if cedar_required { + auth.authorize( + "orchestrator", + "execute", + &format!("Node::\"{}\"", node.id.0), + )?; + } + + // 2. Extract inputs from PipelineContext + let input_caps: Vec = node.input_schemas.keys().cloned().collect(); + let inputs = ctx.extract_inputs(&input_caps).await?; + + // 3. Fetch Vault credentials + let creds = vault + .get_node_credentials(&node.id, node.timeout_secs) + .await?; + + // 4. Record step start + let step_start = StepRecord::start(node.id.clone()); + state.record_step(&ctx.run_id, &step_start).await?; + + // 5. Execute with retry + let result = execute_with_retry(&node, &inputs, &creds, &exec).await; + + match result { + Ok(outputs) => { + let deposited: Vec = outputs.keys().cloned().collect(); + for (cap, value) in outputs { + ctx.deposit(&cap, value).await?; + } + state + .record_step(&ctx.run_id, &step_start.succeed(deposited)) + .await?; + Ok(()) + } + Err(e) => { + // Revoke credentials before propagating error + if let Err(rev_err) = vault.revoke_credentials(&creds.lease_id).await { + warn!( + "failed to revoke credentials for node '{}': {rev_err}", + node.id + ); + } + let err_str = e.to_string(); + state + .record_step(&ctx.run_id, &step_start.fail(err_str.clone())) + .await?; + Err(anyhow!("node '{}' failed: {err_str}", node.id)) + } + } +} + +async fn execute_with_retry( + node: &ActionNode, + inputs: &HashMap, + creds: &NodeCredentials, + exec: &NuExecutor, +) -> Result> { + let mut last_err = None; + + for attempt in 0..=node.retry.max { + match exec.run(node, inputs, creds, node.timeout_secs).await { + Ok(outputs) => return Ok(outputs), + Err(e) if attempt < node.retry.max => { + let delay = backoff_delay(&node.retry, attempt); + warn!( + "node '{}' attempt {}/{} failed, retrying in {:?}: {e}", + node.id, + attempt + 1, + node.retry.max + 1, + delay + ); + last_err = Some(e); + tokio::time::sleep(delay).await; + } + Err(e) => return Err(e), + } + } + + Err(last_err.unwrap_or_else(|| anyhow!("retry exhausted with no error — should not happen"))) +} + +fn backoff_delay(policy: &stratum_graph::RetryPolicy, attempt: u32) -> Duration { + let base = Duration::from_secs(policy.backoff_secs); + match policy.strategy { + BackoffStrategy::Fixed => base, + BackoffStrategy::Linear => base * (attempt + 1), + BackoffStrategy::Exponential => base * 2u32.saturating_pow(attempt), + } +} diff --git a/crates/stratum-orchestrator/tests/test_cedar_deny.rs b/crates/stratum-orchestrator/tests/test_cedar_deny.rs new file mode 100644 index 0000000..bf80f54 --- /dev/null +++ b/crates/stratum-orchestrator/tests/test_cedar_deny.rs @@ -0,0 +1,146 @@ +/// Integration test: Cedar authorization — multi-policy permit/deny scenarios. +use std::io::Write; + +use stratum_orchestrator::auth::CedarAuthorizer; +use tempfile::TempDir; + +fn write_policy(dir: &TempDir, name: &str, content: &str) { + let path = dir.path().join(name); + let mut f = std::fs::File::create(path).unwrap(); + f.write_all(content.as_bytes()).unwrap(); +} + +#[test] +fn test_permit_orchestrator_executes_allowed_node() { + let dir = TempDir::new().unwrap(); + write_policy( + &dir, + "allow.cedar", + r#"permit( + principal == User::"orchestrator", + action == Action::"execute", + resource == Node::"lint" +);"#, + ); + + let authz = CedarAuthorizer::load_from_dir(dir.path()).unwrap(); + authz + .authorize("orchestrator", "execute", "Node::\"lint\"") + .unwrap(); +} + +#[test] +fn test_deny_unknown_principal() { + let dir = TempDir::new().unwrap(); + write_policy( + &dir, + "allow.cedar", + r#"permit( + principal == User::"orchestrator", + action == Action::"execute", + resource == Node::"lint" +);"#, + ); + + let authz = CedarAuthorizer::load_from_dir(dir.path()).unwrap(); + // Unknown principal — no matching permit → implicit deny + let err = authz + .authorize("rogue-agent", "execute", "Node::\"lint\"") + .unwrap_err(); + assert!(err.to_string().contains("Cedar denied")); +} + +#[test] +fn test_deny_wrong_resource() { + let dir = TempDir::new().unwrap(); + write_policy( + &dir, + "allow.cedar", + r#"permit( + principal == User::"orchestrator", + action == Action::"execute", + resource == Node::"lint" +);"#, + ); + + let authz = CedarAuthorizer::load_from_dir(dir.path()).unwrap(); + let err = authz + .authorize("orchestrator", "execute", "Node::\"build\"") + .unwrap_err(); + assert!(err.to_string().contains("Cedar denied")); +} + +#[test] +fn test_explicit_forbid_overrides_permit() { + let dir = TempDir::new().unwrap(); + write_policy( + &dir, + "00_permit.cedar", + r#"permit( + principal == User::"orchestrator", + action == Action::"execute", + resource == Node::"lint" +);"#, + ); + write_policy( + &dir, + "01_forbid.cedar", + r#"forbid(principal, action, resource);"#, + ); + + let authz = CedarAuthorizer::load_from_dir(dir.path()).unwrap(); + // Cedar: forbid overrides permit + let err = authz + .authorize("orchestrator", "execute", "Node::\"lint\"") + .unwrap_err(); + assert!(err.to_string().contains("Cedar denied")); +} + +#[test] +fn test_empty_policy_dir_returns_error() { + let dir = TempDir::new().unwrap(); + let err = match CedarAuthorizer::load_from_dir(dir.path()) { + Err(e) => e, + Ok(_) => panic!("expected load_from_dir to fail on empty directory"), + }; + assert!( + err.to_string().contains("no .cedar policy files"), + "expected 'no .cedar policy files' error, got: {err}" + ); +} + +#[test] +fn test_multiple_permits_any_matching_allows() { + let dir = TempDir::new().unwrap(); + write_policy( + &dir, + "nodes.cedar", + r#"permit( + principal == User::"orchestrator", + action == Action::"execute", + resource == Node::"lint" +); +permit( + principal == User::"orchestrator", + action == Action::"execute", + resource == Node::"build" +); +permit( + principal == User::"orchestrator", + action == Action::"execute", + resource == Node::"install" +);"#, + ); + + let authz = CedarAuthorizer::load_from_dir(dir.path()).unwrap(); + for node in ["lint", "build", "install"] { + authz + .authorize("orchestrator", "execute", &format!("Node::\"{node}\"")) + .unwrap_or_else(|e| panic!("node '{node}' should be permitted: {e}")); + } + + // Node not in the permit list → deny + authz + .authorize("orchestrator", "execute", "Node::\"notify\"") + .unwrap_err(); +} diff --git a/crates/stratum-orchestrator/tests/test_full_pipeline.rs b/crates/stratum-orchestrator/tests/test_full_pipeline.rs new file mode 100644 index 0000000..282fb7c --- /dev/null +++ b/crates/stratum-orchestrator/tests/test_full_pipeline.rs @@ -0,0 +1,172 @@ +/// Integration test: ActionGraph + PipelineContext + StateTracker working together. +/// +/// Simulates a two-stage pipeline data plane (lint → build) without requiring +/// Vault, Nu executors, or external services. +use std::{path::PathBuf, sync::Arc}; + +use stratum_graph::{ + types::{ActionNode, BackoffStrategy, Capability, NodeId, RetryPolicy}, + ActionGraph, +}; +use stratum_orchestrator::context::PipelineContext; +use stratum_state::{InMemoryStateTracker, PipelineStatus, StateTracker}; + +fn cap(s: &str) -> Capability { + Capability(s.to_owned()) +} + +fn node(id: &str, inputs: &[&str], outputs: &[&str], triggers: &[&str]) -> ActionNode { + ActionNode { + id: NodeId(id.to_owned()), + handler: PathBuf::from(format!("scripts/nu/{id}.nu")), + input_schemas: inputs + .iter() + .map(|c| (cap(c), PathBuf::from(format!("schemas/{c}.ncl")))) + .collect(), + output_schemas: outputs + .iter() + .map(|c| (cap(c), PathBuf::from(format!("schemas/{c}.ncl")))) + .collect(), + compensate: None, + retry: RetryPolicy { + max: 0, + backoff_secs: 1, + strategy: BackoffStrategy::Fixed, + }, + timeout_secs: 30, + atomic: false, + triggers: triggers.iter().map(|t| t.to_string()).collect(), + } +} + +async fn make_ctx() -> (PipelineContext, Arc) { + let state = Arc::new(InMemoryStateTracker::new()); + let ctx = PipelineContext::new( + "dev.crate.foo.modified".to_string(), + serde_json::json!({"crate": "foo"}), + Arc::clone(&state) as Arc, + PathBuf::from("/tmp/no-schemas"), + ) + .await + .unwrap(); + (ctx, state) +} + +#[tokio::test] +async fn test_two_stage_pipeline_data_plane() { + let (ctx, state) = make_ctx().await; + + // Build the graph: lint (stage 0) → build (stage 1) + let nodes = vec![ + node("lint", &[], &["linted"], &["dev.crate.>", "dev.>", "dev.crate.foo.modified"]), + node("build", &["linted"], &["built"], &[]), + ]; + let graph = ActionGraph::from_nodes(nodes).unwrap(); + + // Plan the graph for our trigger subject + let stages = graph + .plan("dev.crate.foo.modified", &ctx.trigger_payload) + .unwrap(); + assert_eq!(stages.len(), 2, "expected two stages"); + assert_eq!(stages[0], vec![NodeId("lint".into())]); + assert_eq!(stages[1], vec![NodeId("build".into())]); + + // ── Stage 0: lint ────────────────────────────────────────────────────── + // lint has no inputs; deposits "linted" capability + let linted_value = serde_json::json!({"warnings": 0, "errors": 0}); + ctx.deposit(&cap("linted"), linted_value.clone()) + .await + .unwrap(); + + // ── Stage 1: build ───────────────────────────────────────────────────── + // build reads "linted" produced by stage 0 + let inputs = ctx.extract_inputs(&[cap("linted")]).await.unwrap(); + assert_eq!(inputs[&cap("linted")], linted_value); + + let built_value = serde_json::json!({"artifact": "target/release/foo", "size_bytes": 4096}); + ctx.deposit(&cap("built"), built_value.clone()).await.unwrap(); + + // ── Final state assertions ───────────────────────────────────────────── + state + .update_status(&ctx.run_id, PipelineStatus::Success) + .await + .unwrap(); + + let run = state.get_run(&ctx.run_id).await.unwrap().unwrap(); + assert_eq!(run.status, PipelineStatus::Success); + assert_eq!(run.trigger_subject, "dev.crate.foo.modified"); + + // Both capabilities must be retrievable + let loaded_linted = state + .load_capability(&ctx.run_id, &cap("linted")) + .await + .unwrap(); + let loaded_built = state + .load_capability(&ctx.run_id, &cap("built")) + .await + .unwrap(); + assert_eq!(loaded_linted, Some(linted_value)); + assert_eq!(loaded_built, Some(built_value)); +} + +#[tokio::test] +async fn test_graph_plan_wildcard_subject_routing() { + let nodes = vec![ + node("lint", &[], &["linted"], &["dev.crate.>"]), + node("build", &["linted"], &["built"], &[]), + ]; + let graph = ActionGraph::from_nodes(nodes).unwrap(); + + // Any subject matching `dev.crate.>` should produce a plan + for subject in ["dev.crate.foo.modified", "dev.crate.bar.changed", "dev.crate.x"] { + let stages = graph.plan(subject, &serde_json::Value::Null).unwrap(); + assert!(!stages.is_empty(), "no plan for subject '{subject}'"); + assert_eq!(stages[0][0], NodeId("lint".into())); + } + + // Subject not matching should return an error + let err = graph + .plan("prod.deploy.triggered", &serde_json::Value::Null) + .unwrap_err(); + assert!(err.to_string().contains("no entry nodes match")); +} + +#[tokio::test] +async fn test_multiple_contexts_share_state() { + // Two independent pipelines on the same StateTracker must not interfere. + let state = Arc::new(InMemoryStateTracker::new()); + + let ctx_a = PipelineContext::new( + "dev.crate.a.modified".to_string(), + serde_json::Value::Null, + Arc::clone(&state) as Arc, + PathBuf::from("/tmp/no-schemas"), + ) + .await + .unwrap(); + + let ctx_b = PipelineContext::new( + "dev.crate.b.modified".to_string(), + serde_json::Value::Null, + Arc::clone(&state) as Arc, + PathBuf::from("/tmp/no-schemas"), + ) + .await + .unwrap(); + + let linted_a = serde_json::json!({"crate": "a", "warnings": 0}); + let linted_b = serde_json::json!({"crate": "b", "warnings": 2}); + + ctx_a.deposit(&cap("linted"), linted_a.clone()).await.unwrap(); + ctx_b.deposit(&cap("linted"), linted_b.clone()).await.unwrap(); + + // Each context sees its own value, not the other's + assert_eq!(ctx_a.extract(&cap("linted")).await.unwrap(), linted_a); + assert_eq!(ctx_b.extract(&cap("linted")).await.unwrap(), linted_b); + + // Verify via StateTracker too + let val_a = state.load_capability(&ctx_a.run_id, &cap("linted")).await.unwrap(); + let val_b = state.load_capability(&ctx_b.run_id, &cap("linted")).await.unwrap(); + assert_eq!(val_a, Some(linted_a)); + assert_eq!(val_b, Some(linted_b)); +} diff --git a/crates/stratum-orchestrator/tests/test_saga_compensation.rs b/crates/stratum-orchestrator/tests/test_saga_compensation.rs new file mode 100644 index 0000000..a65e203 --- /dev/null +++ b/crates/stratum-orchestrator/tests/test_saga_compensation.rs @@ -0,0 +1,149 @@ +/// Integration test: Saga compensation state machine. +/// +/// Validates the state transition sequence: +/// Running → (stage 1 succeeds) → Compensating → Compensated +/// and that capabilities deposited in stage 1 remain accessible during compensation. +use std::{path::PathBuf, sync::Arc}; + +use stratum_graph::types::Capability; +use stratum_orchestrator::context::PipelineContext; +use stratum_state::{InMemoryStateTracker, PipelineStatus, StateTracker, StepRecord}; +use stratum_graph::types::NodeId; + +fn cap(s: &str) -> Capability { + Capability(s.to_owned()) +} + +async fn make_ctx() -> (PipelineContext, Arc) { + let state = Arc::new(InMemoryStateTracker::new()); + let ctx = PipelineContext::new( + "dev.crate.foo.modified".to_string(), + serde_json::Value::Null, + Arc::clone(&state) as Arc, + PathBuf::from("/tmp/no-schemas"), + ) + .await + .unwrap(); + (ctx, state) +} + +#[tokio::test] +async fn test_compensation_state_transitions() { + let (ctx, state) = make_ctx().await; + + // Stage 0 (lint) succeeds — deposit capability and record step + let lint_node = NodeId("lint".into()); + let step_start = StepRecord::start(lint_node.clone()); + state.record_step(&ctx.run_id, &step_start).await.unwrap(); + + let linted_value = serde_json::json!({"warnings": 0}); + ctx.deposit(&cap("linted"), linted_value.clone()) + .await + .unwrap(); + + let step_done = step_start.succeed(vec![cap("linted")]); + state.record_step(&ctx.run_id, &step_done).await.unwrap(); + + // Stage 1 (build) fails — transition to Compensating + state + .update_status(&ctx.run_id, PipelineStatus::Compensating) + .await + .unwrap(); + + let run = state.get_run(&ctx.run_id).await.unwrap().unwrap(); + assert_eq!(run.status, PipelineStatus::Compensating); + + // Compensation: lint's rollback reads the "linted" capability from DB + // (in production NuExecutor would call the compensation script; + // here we verify the data is accessible) + let linted_in_db = state + .load_capability(&ctx.run_id, &cap("linted")) + .await + .unwrap(); + assert_eq!( + linted_in_db, + Some(linted_value), + "linted capability must survive into compensation phase" + ); + + // Compensation completes + state + .update_status(&ctx.run_id, PipelineStatus::Compensated) + .await + .unwrap(); + + let run = state.get_run(&ctx.run_id).await.unwrap().unwrap(); + assert_eq!(run.status, PipelineStatus::Compensated); + + // Step record must show lint succeeded before compensation started + assert_eq!(run.steps.len(), 1, "exactly one step record (lint)"); + assert_eq!(run.steps[0].node_id, lint_node); + assert_eq!(run.steps[0].capabilities_deposited, vec![cap("linted")]); +} + +#[tokio::test] +async fn test_step_upsert_within_saga() { + let (ctx, state) = make_ctx().await; + + let node_id = NodeId("fmt".into()); + + // Record start, then update to success (upsert must not duplicate) + let start = StepRecord::start(node_id.clone()); + state.record_step(&ctx.run_id, &start).await.unwrap(); + + let done = start.succeed(vec![cap("formatted")]); + state.record_step(&ctx.run_id, &done).await.unwrap(); + + let run = state.get_run(&ctx.run_id).await.unwrap().unwrap(); + assert_eq!(run.steps.len(), 1, "upsert must not duplicate step records"); + assert_eq!( + run.steps[0].capabilities_deposited, + vec![cap("formatted")], + "step must reflect the succeeded record" + ); +} + +#[tokio::test] +async fn test_multiple_stages_compensated_in_order() { + let (ctx, state) = make_ctx().await; + + // Simulate two successful stages before failure + for (name, cap_name) in [("lint", "linted"), ("fmt", "formatted")] { + let step = StepRecord::start(NodeId(name.into())); + state.record_step(&ctx.run_id, &step).await.unwrap(); + + ctx.deposit(&cap(cap_name), serde_json::json!({"ok": true})) + .await + .unwrap(); + + let done = step.succeed(vec![cap(cap_name)]); + state.record_step(&ctx.run_id, &done).await.unwrap(); + } + + // Third stage fails → compensate + state + .update_status(&ctx.run_id, PipelineStatus::Compensating) + .await + .unwrap(); + + // Both capabilities from stages 0 and 1 must remain accessible + for cap_name in ["linted", "formatted"] { + let v = state + .load_capability(&ctx.run_id, &cap(cap_name)) + .await + .unwrap(); + assert!( + v.is_some(), + "capability '{cap_name}' must be accessible during compensation" + ); + } + + state + .update_status(&ctx.run_id, PipelineStatus::Compensated) + .await + .unwrap(); + + let run = state.get_run(&ctx.run_id).await.unwrap().unwrap(); + assert_eq!(run.status, PipelineStatus::Compensated); + assert_eq!(run.steps.len(), 2, "two step records from two stages"); +} diff --git a/crates/stratum-state/Cargo.toml b/crates/stratum-state/Cargo.toml new file mode 100644 index 0000000..f234b9b --- /dev/null +++ b/crates/stratum-state/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "stratum-state" +version = "0.1.0" +edition.workspace = true +description = "Operational domain: pipeline run state, step records, capability store" +license.workspace = true + +[dependencies] +stratum-graph = { path = "../stratum-graph" } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +thiserror = { workspace = true } +async-trait = { workspace = true } +tokio = { workspace = true } +uuid = { workspace = true } +chrono = { workspace = true, features = ["serde"] } +tracing = { workspace = true } + +surrealdb = { workspace = true, optional = true } + +[features] +default = ["mem-store"] +mem-store = ["surrealdb", "surrealdb/kv-mem"] +rocksdb-store = ["surrealdb", "surrealdb/kv-rocksdb"] + +[dev-dependencies] +tokio-test = { workspace = true } +tempfile = { workspace = true } diff --git a/crates/stratum-state/src/lib.rs b/crates/stratum-state/src/lib.rs new file mode 100644 index 0000000..20eb6d7 --- /dev/null +++ b/crates/stratum-state/src/lib.rs @@ -0,0 +1,8 @@ +pub mod tracker; +pub mod types; + +pub use tracker::{InMemoryStateTracker, StateTracker}; +pub use types::{PipelineRun, PipelineRunId, PipelineStatus, StepRecord, StepStatus}; + +#[cfg(any(feature = "mem-store", feature = "rocksdb-store"))] +pub use tracker::SurrealStateTracker; diff --git a/crates/stratum-state/src/tracker.rs b/crates/stratum-state/src/tracker.rs new file mode 100644 index 0000000..a25420b --- /dev/null +++ b/crates/stratum-state/src/tracker.rs @@ -0,0 +1,343 @@ +use anyhow::Result; +use async_trait::async_trait; + +use stratum_graph::types::Capability; + +use crate::types::{PipelineRun, PipelineRunId, PipelineStatus, StepRecord}; + +#[async_trait] +pub trait StateTracker: Send + Sync { + async fn create_run(&self, run: &PipelineRun) -> Result<()>; + async fn update_status(&self, id: &PipelineRunId, status: PipelineStatus) -> Result<()>; + async fn record_step(&self, id: &PipelineRunId, step: &StepRecord) -> Result<()>; + async fn deposit_capability( + &self, + id: &PipelineRunId, + cap: &Capability, + value: &serde_json::Value, + ) -> Result<()>; + async fn load_capability( + &self, + id: &PipelineRunId, + cap: &Capability, + ) -> Result>; + async fn get_run(&self, id: &PipelineRunId) -> Result>; +} + +/// In-memory implementation — for tests and ephemeral pipelines. +pub struct InMemoryStateTracker { + runs: tokio::sync::RwLock>, + caps: tokio::sync::RwLock>, +} + +impl InMemoryStateTracker { + pub fn new() -> Self { + Self { + runs: tokio::sync::RwLock::new(std::collections::HashMap::new()), + caps: tokio::sync::RwLock::new(std::collections::HashMap::new()), + } + } + + fn cap_key(id: &PipelineRunId, cap: &Capability) -> String { + format!("{}:{}", id.0, cap.0) + } +} + +impl Default for InMemoryStateTracker { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl StateTracker for InMemoryStateTracker { + async fn create_run(&self, run: &PipelineRun) -> Result<()> { + self.runs + .write() + .await + .insert(run.id.0.to_string(), run.clone()); + Ok(()) + } + + async fn update_status(&self, id: &PipelineRunId, status: PipelineStatus) -> Result<()> { + if let Some(run) = self.runs.write().await.get_mut(&id.0.to_string()) { + run.status = status; + } + Ok(()) + } + + async fn record_step(&self, id: &PipelineRunId, step: &StepRecord) -> Result<()> { + if let Some(run) = self.runs.write().await.get_mut(&id.0.to_string()) { + // Replace existing step record for this node if present + if let Some(pos) = run.steps.iter().position(|s| s.node_id == step.node_id) { + run.steps[pos] = step.clone(); + } else { + run.steps.push(step.clone()); + } + } + Ok(()) + } + + async fn deposit_capability( + &self, + id: &PipelineRunId, + cap: &Capability, + value: &serde_json::Value, + ) -> Result<()> { + self.caps + .write() + .await + .insert(Self::cap_key(id, cap), value.clone()); + Ok(()) + } + + async fn load_capability( + &self, + id: &PipelineRunId, + cap: &Capability, + ) -> Result> { + Ok(self.caps.read().await.get(&Self::cap_key(id, cap)).cloned()) + } + + async fn get_run(&self, id: &PipelineRunId) -> Result> { + Ok(self.runs.read().await.get(&id.0.to_string()).cloned()) + } +} + +/// SurrealDB-backed implementation — persistent, survives restarts. +#[cfg(any(feature = "mem-store", feature = "rocksdb-store"))] +pub use surreal_tracker::SurrealStateTracker; + +#[cfg(any(feature = "mem-store", feature = "rocksdb-store"))] +mod surreal_tracker { + use anyhow::{Context, Result}; + use async_trait::async_trait; + use serde::Serialize; + #[cfg(feature = "mem-store")] + use surrealdb::engine::local::Mem; + use surrealdb::{engine::local::Db, Surreal}; + + use stratum_graph::types::Capability; + + use crate::types::{PipelineRun, PipelineRunId, PipelineStatus, StepRecord}; + + use super::StateTracker; + + /// `Db` is the `Connection` implementor for all embedded SurrealDB engines. + /// `Mem` / `RocksDb` are engine *tokens* passed to `Surreal::new::()`. + pub struct SurrealStateTracker { + db: Surreal, + } + + impl SurrealStateTracker { + /// Embedded in-memory instance — test / ephemeral use. + #[cfg(feature = "mem-store")] + pub async fn new_mem() -> Result { + let db = Surreal::new::(()) + .await + .context("creating in-memory SurrealDB")?; + db.use_ns("stratum").use_db("state").await?; + Ok(Self { db }) + } + + /// Embedded RocksDB instance — persistent across restarts. + #[cfg(feature = "rocksdb-store")] + pub async fn new_rocksdb(path: impl AsRef) -> Result { + use surrealdb::engine::local::RocksDb; + let db = Surreal::new::(path.as_ref()) + .await + .context("creating RocksDB SurrealDB")?; + db.use_ns("stratum").use_db("state").await?; + Ok(Self { db }) + } + } + + /// Route all DB I/O through `serde_json::Value`, which implements SurrealDB 3's + /// `SurrealValue` trait. Custom types don't need to implement `SurrealValue`. + fn to_json(v: &T) -> Result { + serde_json::to_value(v).context("serializing to serde_json::Value") + } + + fn from_json(v: serde_json::Value) -> Result { + serde_json::from_value(v).context("deserializing from serde_json::Value") + } + + #[async_trait] + impl StateTracker for SurrealStateTracker { + async fn create_run(&self, run: &PipelineRun) -> Result<()> { + let id = run.id.0.to_string(); + let payload = to_json(run)?; + let _: Option = self + .db + .upsert(("pipeline_run_v1", id)) + .content(payload) + .await + .context("create_run")?; + Ok(()) + } + + async fn update_status(&self, id: &PipelineRunId, status: PipelineStatus) -> Result<()> { + let id_str = id.0.to_string(); + let patch = to_json(&serde_json::json!({ "status": status }))?; + let _: Option = self + .db + .update(("pipeline_run_v1", id_str)) + .merge(patch) + .await + .context("update_status")?; + Ok(()) + } + + async fn record_step(&self, id: &PipelineRunId, step: &StepRecord) -> Result<()> { + let id_str = id.0.to_string(); + let raw: Option = self + .db + .select(("pipeline_run_v1", id_str.clone())) + .await + .context("record_step: get run")?; + + if let Some(raw) = raw { + let mut run: PipelineRun = from_json(raw)?; + if let Some(pos) = run.steps.iter().position(|s| s.node_id == step.node_id) { + run.steps[pos] = step.clone(); + } else { + run.steps.push(step.clone()); + } + let payload = to_json(&run)?; + let _: Option = self + .db + .upsert(("pipeline_run_v1", id_str)) + .content(payload) + .await + .context("record_step: update run")?; + } + Ok(()) + } + + async fn deposit_capability( + &self, + id: &PipelineRunId, + cap: &Capability, + value: &serde_json::Value, + ) -> Result<()> { + let record_key = format!("{}-{}", id.0, cap.0.replace(' ', "_")); + let payload = serde_json::json!({ + "run_id": id.0.to_string(), + "capability": cap.0.clone(), + "value": value, + }); + let _: Option = self + .db + .upsert(("capability_store_v1", record_key)) + .content(payload) + .await + .context("deposit_capability")?; + Ok(()) + } + + async fn load_capability( + &self, + id: &PipelineRunId, + cap: &Capability, + ) -> Result> { + let record_key = format!("{}-{}", id.0, cap.0.replace(' ', "_")); + let raw: Option = self + .db + .select(("capability_store_v1", record_key)) + .await + .context("load_capability")?; + Ok(raw.and_then(|mut r| r.get_mut("value").map(|v| v.take()))) + } + + async fn get_run(&self, id: &PipelineRunId) -> Result> { + let id_str = id.0.to_string(); + let raw: Option = self + .db + .select(("pipeline_run_v1", id_str)) + .await + .context("get_run")?; + raw.map(from_json).transpose() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{PipelineRun, StepRecord}; + use stratum_graph::types::{Capability, NodeId}; + + #[tokio::test] + async fn test_create_and_get_run() { + let tracker = InMemoryStateTracker::new(); + let run = PipelineRun::new( + "dev.crate.foo.modified".to_string(), + serde_json::Value::Null, + ); + let id = run.id.clone(); + + tracker.create_run(&run).await.unwrap(); + let fetched = tracker.get_run(&id).await.unwrap(); + assert!(fetched.is_some()); + assert_eq!(fetched.unwrap().trigger_subject, "dev.crate.foo.modified"); + } + + #[tokio::test] + async fn test_deposit_and_load_capability() { + let tracker = InMemoryStateTracker::new(); + let run = PipelineRun::new("test".to_string(), serde_json::Value::Null); + let id = run.id.clone(); + tracker.create_run(&run).await.unwrap(); + + let cap = Capability("linted".to_string()); + let value = serde_json::json!({"status": "clean", "warnings": 0}); + + tracker.deposit_capability(&id, &cap, &value).await.unwrap(); + + let loaded = tracker.load_capability(&id, &cap).await.unwrap(); + assert_eq!(loaded, Some(value)); + } + + #[tokio::test] + async fn test_status_transitions() { + let tracker = InMemoryStateTracker::new(); + let run = PipelineRun::new("test".to_string(), serde_json::Value::Null); + let id = run.id.clone(); + tracker.create_run(&run).await.unwrap(); + + tracker + .update_status(&id, PipelineStatus::AwaitingAgent) + .await + .unwrap(); + tracker + .update_status(&id, PipelineStatus::Running) + .await + .unwrap(); + tracker + .update_status(&id, PipelineStatus::Success) + .await + .unwrap(); + + let run = tracker.get_run(&id).await.unwrap().unwrap(); + assert_eq!(run.status, PipelineStatus::Success); + } + + #[tokio::test] + async fn test_record_step_upsert() { + let tracker = InMemoryStateTracker::new(); + let run = PipelineRun::new("test".to_string(), serde_json::Value::Null); + let id = run.id.clone(); + tracker.create_run(&run).await.unwrap(); + + let node_id = NodeId("lint".to_string()); + let step = StepRecord::start(node_id.clone()); + tracker.record_step(&id, &step).await.unwrap(); + + let step_done = step.succeed(vec![Capability("linted".to_string())]); + tracker.record_step(&id, &step_done).await.unwrap(); + + let run = tracker.get_run(&id).await.unwrap().unwrap(); + assert_eq!(run.steps.len(), 1); // upserted, not duplicated + assert_eq!(run.steps[0].capabilities_deposited.len(), 1); + } +} diff --git a/crates/stratum-state/src/types.rs b/crates/stratum-state/src/types.rs new file mode 100644 index 0000000..4fec8be --- /dev/null +++ b/crates/stratum-state/src/types.rs @@ -0,0 +1,108 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use stratum_graph::types::{Capability, NodeId}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PipelineRunId(pub Uuid); + +impl PipelineRunId { + pub fn new() -> Self { + Self(Uuid::new_v4()) + } +} + +impl Default for PipelineRunId { + fn default() -> Self { + Self::new() + } +} + +impl std::fmt::Display for PipelineRunId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PipelineStatus { + Running, + AwaitingAgent, + Success, + Failed, + Compensating, + Compensated, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum StepStatus { + Running, + Success, + Failed, + Compensated, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PipelineRun { + pub id: PipelineRunId, + pub trigger_subject: String, + pub trigger_payload: serde_json::Value, + pub status: PipelineStatus, + pub started_at: DateTime, + pub finished_at: Option>, + pub steps: Vec, +} + +impl PipelineRun { + pub fn new(trigger_subject: String, trigger_payload: serde_json::Value) -> Self { + Self { + id: PipelineRunId::new(), + trigger_subject, + trigger_payload, + status: PipelineStatus::Running, + started_at: Utc::now(), + finished_at: None, + steps: vec![], + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StepRecord { + pub node_id: NodeId, + pub status: StepStatus, + pub started_at: DateTime, + pub finished_at: Option>, + pub capabilities_deposited: Vec, + pub error: Option, +} + +impl StepRecord { + pub fn start(node_id: NodeId) -> Self { + Self { + node_id, + status: StepStatus::Running, + started_at: Utc::now(), + finished_at: None, + capabilities_deposited: vec![], + error: None, + } + } + + pub fn succeed(mut self, deposited: Vec) -> Self { + self.status = StepStatus::Success; + self.finished_at = Some(Utc::now()); + self.capabilities_deposited = deposited; + self + } + + pub fn fail(mut self, error: String) -> Self { + self.status = StepStatus::Failed; + self.finished_at = Some(Utc::now()); + self.error = Some(error); + self + } +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..7b192d6 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,43 @@ +version: "3.9" + +# Stratum dev stack — SurrealDB + Zot OCI registry +# NATS is assumed running via provisioning docker-compose on the same host network. +# Start: docker-compose -f docker-compose.dev.yml up -d + +networks: + stratum-dev: + driver: bridge + +services: + surrealdb-stratum: + image: surrealdb/surrealdb:v2 + command: start --log info --user root --pass root memory + ports: + - "8100:8000" + networks: + - stratum-dev + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8000/health"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 5s + + zot-stratum: + image: ghcr.io/project-zot/zot-linux-amd64:latest + ports: + - "5000:5000" + volumes: + - ./config/zot-config.json:/etc/zot/config.json:ro + - zot-data:/var/lib/registry + networks: + - stratum-dev + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:5000/v2/"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 5s + +volumes: + zot-data: diff --git a/docs/en/architecture/README.md b/docs/en/architecture/README.md index 520e38d..c092f15 100644 --- a/docs/en/architecture/README.md +++ b/docs/en/architecture/README.md @@ -10,6 +10,7 @@ Documented architectural decisions following the ADR format: - [**ADR-001: Stratum-Embeddings**](adrs/001-stratum-embeddings.md) - Unified embedding library - [**ADR-002: Stratum-LLM**](adrs/002-stratum-llm.md) - Unified LLM provider library +- [**ADR-003: Stratum-Orchestrator**](adrs/003-stratum-orchestrator.md) - Graph-driven workflow orchestrator ## ADR Format diff --git a/docs/en/architecture/adrs/003-stratum-orchestrator.md b/docs/en/architecture/adrs/003-stratum-orchestrator.md new file mode 100644 index 0000000..c6d900b --- /dev/null +++ b/docs/en/architecture/adrs/003-stratum-orchestrator.md @@ -0,0 +1,179 @@ +# ADR-003: Stratum-Orchestrator — Graph-Driven Workflow Orchestrator + +**Status**: Accepted +**Date**: 2026-02-20 + +## Context + +The StratumIOps ecosystem spans multiple active projects (provisioning, kogral, syntaxis, typedialog) that evolve concurrently with cross-project dependencies. Each project triggers build, validation, publish, and notification workflows in response to code changes. Before this decision, these workflows were: + +- Hardcoded per-project scripts with no shared execution model +- Not auditable — no durable record of which step ran, in what order, with what outcome +- Not composable — a workflow in `provisioning` could not react to an event from `kogral` +- Not safe — no atomicity, no rollback on partial failure, no credential scoping +- Not scalable — adding a new project meant copying and adapting scripts, accumulating drift + +The core requirement is a **cross-project, event-driven, agnostic workflow orchestrator** that can coordinate build pipelines, AI agent tasks, and infrastructure operations without being modified for each new project or provider. + +Two design directions were evaluated: + +| Approach | Description | Problem | +|----------|-------------|---------| +| **Static rule router** | Map NATS subject patterns to scripts in a config file | Rules multiply with projects; changing a workflow requires editing the router | +| **Graph-driven engine** | Events traverse a DAG of action nodes declared in Nickel; orchestrator is agnostic | Orchestrator never changes for new workflows | + +The graph-driven approach was chosen. The orchestrator loads node definitions from Nickel files, builds an in-memory `ActionGraph`, and executes pipelines by traversing the graph — it does not know what the pipeline does, only how to coordinate it. + +## Fundamental Design Characteristics + +### 1. Graph-Driven Execution, Not Static Routing + +The orchestrator does not contain routing tables. Instead, NATS subject patterns are matched against an `ActionGraph` built from Nickel node definitions. Each `ActionNode` declares: +- `trigger`: NATS subject patterns that can activate this node as an entry point +- `input_schemas`: capabilities required before execution +- `output_schemas`: capabilities produced after execution +- `compensate`: optional rollback script for Saga atomicity + +The graph is built by traversal of producer/consumer capability indexes. Topological sort produces a staged execution plan. **Adding a new workflow means adding `.ncl` files — the orchestrator binary never changes.** + +### 2. Stateless Orchestrator — DB-First State + +The orchestrator process holds no durable state. Every `PipelineContext` write goes to SurrealDB first, then updates an in-memory `DashMap` cache. On crash, a restarted instance reconstructs the cache from the DB. This enables: + +- **Horizontal scaling**: multiple orchestrator instances share one SurrealDB, no split-brain +- **Crash recovery**: pipelines resume from last persisted capability, not from the beginning +- **Observability**: full pipeline state is queryable at any point without instrumenting the process + +### 3. Nickel as Single Source of Truth — No Dual Truth + +Action nodes, capability schemas, and the orchestrator startup config are all defined in Nickel. There is no separate indexing process, no database copy of node definitions used at runtime. The `ActionGraph` is built in-memory from `.ncl` files at startup via `nickel export --format json`, then kept live via a `notify` file watcher for hot-reload. + +This eliminates the dual-truth problem: the Nickel file IS the definition. No risk of a database record diverging from the file on disk. + +### 4. Capability Model — Dependency Inversion at Execution Level + +Nodes do not depend on each other directly. They declare capabilities they produce and consume. The graph engine resolves dependencies: + +``` +lint-crate → produces: linted-code +fmt-crate → produces: formatted-code +build-crate → consumes: linted-code, formatted-code + → produces: built-artifact +install-crate → consumes: built-artifact +``` + +This is Dependency Inversion applied to the execution domain: `build-crate` does not know about `lint-crate`. It only knows it needs `linted-code`. Any node that produces `linted-code` satisfies the dependency. Nodes can be swapped, replaced, or parallelized without changing their consumers. + +### 5. Three Independent Auth Planes + +Authentication and authorization are split across three independent, non-substitutable planes: + +| Plane | Technology | Scope | What it controls | +|-------|-----------|-------|-----------------| +| **Publisher auth** | NATS NKeys (ed25519) | Transport | Who can publish events to `dev.>` subjects | +| **Workflow authz** | Cedar policies | Orchestrator | Which pipelines/nodes a principal can trigger | +| **Execution credentials** | Vault (SecretumVault) | Per-node, per-step | Scoped secrets with TTL = node timeout | + +Credentials from Vault are injected as environment variables into the Nushell subprocess and revoked on node failure. They never appear in NATS messages, logs, or `PipelineContext` (redacted before storage). + +### 6. Saga Atomicity — Compensation, Not Transactions + +Pipelines execute forward through stages. If a stage fails, the orchestrator does not roll back a database transaction — it runs `compensate.nu` scripts in reverse order through all previously successful stages. This is the Saga pattern: + +``` +Stage 0: lint (ok) + fmt (ok) → executed +Stage 1: build (FAIL) → trigger compensation +Stage 0 compensation: → undo lint, undo fmt (in parallel, reverse) +``` + +Compensation is best-effort: compensation failures are logged but do not block the pipeline from reaching `Compensated` status. The DB record captures the full compensation trace. + +### 7. Parallel Stages via JoinSet + CancellationToken + +Within each stage, nodes with no capability dependencies on each other execute in parallel using `tokio::task::JoinSet`. Fail-fast is implemented via `CancellationToken`: the first node failure cancels the token, aborting all sibling tasks in the stage. + +``` +Stage 0: [lint-crate ‖ fmt-crate] — parallel (no inter-dependency) +Stage 1: [build-crate] — sequential (needs both capabilities) +Stage 2: [install-crate] — sequential (needs built-artifact) +``` + +### 8. OCI for Everything — Content-Addressed Artifacts + +Both node definitions and the Nickel base library are published as OCI artifacts to a Zot registry. The publish pipeline for each is: `nickel typecheck` → `gitleaks detect` → `nickel export` → `sha256sum` → `oras push` with content-hash annotations. + +The `ncl-import-resolver` binary bridges OCI → local filesystem: it pulls each referenced OCI layer at orchestrator startup, verifies the digest against the annotated hash, then exposes a local path for Nickel imports. This prevents loading unverified or tampered node definitions. + +This follows the same model as container images: build → scan → publish → consume by digest. + +### 9. Nushell as Execution Unit — Agnostic by Design + +Each action node's `handler` is a Nushell script. The executor spawns `nu --no-config-file `, passes `PipelineContext` inputs as JSON on stdin, and reads the output JSON from stdout. This makes execution: + +- **Domain-agnostic**: the orchestrator has no knowledge of what the script does +- **Hot-replaceable**: updating a workflow means replacing a `.nu` file, not recompiling the binary +- **Sandboxable**: each node runs in its own process with scoped Vault credentials +- **Testable independently**: scripts can be invoked directly with `echo '{}' | nu script.nu` + +### 10. TypeDialog Scope — Startup Config Only + +TypeDialog is used exclusively for orchestrator startup configuration (SurrealDB URL, NATS URL, Zot URL, Vault URL, log level, feature flags). It is **not** used for project declaratives, workflow definitions, or node configurations. Those live in Nickel files managed per project. This prevents TypeDialog from becoming a catch-all config tool and keeps its scope bounded. + +## Decision + +`stratum-orchestrator` is implemented as a new crate family in the StratumIOps monorepo: + +| Crate | Domain | Responsibility | +|-------|--------|----------------| +| `stratum-graph` | Knowledge | `ActionNode`, `Capability`, `GraphRepository` trait | +| `stratum-state` | Operational | `PipelineRun`, `StepRecord`, `StateTracker` trait | +| `platform-nats` | Transport | JetStream consumer with NKey auth | +| `stratum-orchestrator` | Coordination | `ActionGraph`, `PipelineContext`, `StageRunner`, auth, executor | + +Domain isolation is structural: `stratum-graph` and `stratum-state` are separate crates with separate SurrealDB table namespaces. `stratum-orchestrator` depends on their traits, not their implementations — compile-time enforcement. + +The orchestrator binary startup sequence: load TypeDialog config → connect SurrealDB → connect NATS → resolve OCI Nickel imports → build ActionGraph → start notify watcher → initialize Cedar policies → start HTTP server (health + agent callback) → enter JetStream pull loop. + +## Rationale + +### Why Not a General-Purpose Workflow Engine (Temporal, Argo, etc.)? + +| Concern | External engine | stratum-orchestrator | +|---------|-----------------|----------------------| +| Cross-project event model | Requires adapter per project | Native NATS subject matching | +| Nickel integration | Not possible | First-class: nodes are `.ncl` files | +| Nushell execution | Not supported | Native subprocess executor | +| Operational footprint | Heavy (Temporal cluster, Argo K8s) | Single binary + SurrealDB + NATS | +| Custom auth model | Difficult to extend | Three planes designed in | + +### Why Saga over 2PC? + +Two-phase commit across distributed Nushell scripts is not feasible — scripts are external processes with no transaction coordinator. Saga compensation scripts (`compensate.nu`) are the only realistic atomicity model for cross-process workflows. The trade-off is accepted: compensation is best-effort, not guaranteed-atomic, but the failure cases are logged and auditable. + +### Why In-Memory ActionGraph vs DB-Persisted Nodes? + +Storing node definitions in SurrealDB creates dual truth. The file on disk and the DB record can diverge. Hot-reload via `notify` on the filesystem is simpler, faster, and eliminates the sync problem. SurrealDB is used only for operational state (pipeline runs, capability stores) — knowledge (node definitions) stays in the filesystem. + +## Consequences + +**Accepted trade-offs**: +- `nickel export` is a subprocess call per file at startup — adds ~50ms per node file to startup time. Mitigated by parallel load with `JoinSet` during startup. +- Saga compensation is best-effort — a compensation script that itself fails is logged but does not block status progression. This is a known Saga trade-off. +- Nushell subprocess overhead per node — each node execution spawns a process. For sub-second scripts this is observable latency. Acceptable for CI/CD and infrastructure workflows. +- OCI layer pull at startup — cold starts require pulling Nickel lib layers. Mitigated by local digest cache in `~/.cache/stratum/ncl/`. + +**Benefits gained**: +- New workflows require only new `.ncl` files — zero orchestrator binary changes +- Full pipeline audit trail in SurrealDB: every step, every capability deposit, every compensation +- Crash recovery is free: restart the orchestrator, pipeline resumes from last persisted state +- Auth is non-negotiable: publisher identity (NKeys), workflow authorization (Cedar), and execution credentials (Vault) are enforced at every pipeline invocation +- Horizontal scaling: stateless orchestrator + shared SurrealDB enables multiple instances on the same event stream + +## References + +- Implementation plan: `.coder/2026-02-20-stratum-orchestrator-plan.plan.md` +- Architecture diagram: `assets/diagrams/arch-stratum-orchestrator.svg` +- Build pipeline flow: `assets/diagrams/flow-stratum-build-pipeline.svg` +- Nickel base library: `nickel/stratum-base/stratum-base.ncl` +- Crates: `crates/stratum-graph/`, `crates/stratum-state/`, `crates/platform-nats/`, `crates/stratum-orchestrator/` +- Related: ADR-001 (stratum-embeddings), ADR-002 (stratum-llm) diff --git a/docs/en/architecture/adrs/README.md b/docs/en/architecture/adrs/README.md index 7516fb5..ad4ac6a 100644 --- a/docs/en/architecture/adrs/README.md +++ b/docs/en/architecture/adrs/README.md @@ -4,10 +4,11 @@ Architecture decision records for the STRATUMIOPS ecosystem. ## Active ADRs -| ID | Title | Status | -| -------------------------------- | --------------------------------------------- | -------- | -| [001](001-stratum-embeddings.md) | Stratum-Embeddings: Unified Embedding Library | Proposed | -| [002](002-stratum-llm.md) | Stratum-LLM: Unified LLM Provider Library | Proposed | +| ID | Title | Status | +| ---------------------------------------- | --------------------------------------------- | -------- | +| [001](001-stratum-embeddings.md) | Stratum-Embeddings: Unified Embedding Library | Proposed | +| [002](002-stratum-llm.md) | Stratum-LLM: Unified LLM Provider Library | Proposed | +| [003](003-stratum-orchestrator.md) | Stratum-Orchestrator: Graph-Driven Workflow Orchestrator | Accepted | ## Statuses diff --git a/docs/es/architecture/README.md b/docs/es/architecture/README.md index 9857f6d..d79373f 100644 --- a/docs/es/architecture/README.md +++ b/docs/es/architecture/README.md @@ -10,6 +10,7 @@ Decisiones arquitecturales documentadas siguiendo el formato ADR: - [**ADR-001: Stratum-Embeddings**](adrs/001-stratum-embeddings.md) - Biblioteca unificada de embeddings - [**ADR-002: Stratum-LLM**](adrs/002-stratum-llm.md) - Biblioteca unificada de providers LLM +- [**ADR-003: Stratum-Orchestrator**](adrs/003-stratum-orchestrator.md) - Orquestador de flujos guiado por grafo ## Formato ADR diff --git a/docs/es/architecture/adrs/003-stratum-orchestrator.md b/docs/es/architecture/adrs/003-stratum-orchestrator.md new file mode 100644 index 0000000..9af24e6 --- /dev/null +++ b/docs/es/architecture/adrs/003-stratum-orchestrator.md @@ -0,0 +1,179 @@ +# ADR-003: Stratum-Orchestrator — Orquestador de Flujos Guiado por Grafo + +**Estado**: Aceptado +**Fecha**: 2026-02-20 + +## Contexto + +El ecosistema StratumIOps abarca múltiples proyectos activos (provisioning, kogral, syntaxis, typedialog) que evolucionan concurrentemente con dependencias cruzadas. Cada proyecto dispara flujos de trabajo de build, validación, publicación y notificación en respuesta a cambios en el código. Antes de esta decisión, dichos flujos eran: + +- Scripts ad-hoc por proyecto sin modelo de ejecución compartido +- No auditables — ningún registro duradero de qué paso se ejecutó, en qué orden, con qué resultado +- No componibles — un flujo en `provisioning` no podía reaccionar a un evento de `kogral` +- No seguros — sin atomicidad, sin rollback en fallo parcial, sin alcance de credenciales +- No escalables — añadir un proyecto nuevo implicaba copiar y adaptar scripts, acumulando deriva + +El requisito central es un **orquestador de flujos agnóstico, dirigido por eventos y multi-proyecto**, capaz de coordinar pipelines de build, tareas de agentes IA y operaciones de infraestructura sin modificarse para cada nuevo proyecto o proveedor. + +Se evaluaron dos enfoques de diseño: + +| Enfoque | Descripción | Problema | +|---------|-------------|---------| +| **Router de reglas estáticas** | Mapear patrones de sujeto NATS a scripts en un fichero de configuración | Las reglas se multiplican con los proyectos; cambiar un flujo exige editar el router | +| **Motor guiado por grafo** | Los eventos atraviesan un DAG de nodos de acción declarados en Nickel; el orquestador es agnóstico | El orquestador nunca cambia para nuevos flujos | + +Se adoptó el enfoque basado en grafo. El orquestador carga definiciones de nodos desde ficheros Nickel, construye un `ActionGraph` en memoria y ejecuta pipelines atravesando el grafo — no sabe qué hace el pipeline, solo cómo coordinarlo. + +## Características Fundamentales del Diseño + +### 1. Ejecución Guiada por Grafo, no Enrutamiento Estático + +El orquestador no contiene tablas de enrutamiento. Los patrones de sujeto NATS se confrontan contra un `ActionGraph` construido a partir de definiciones de nodos en Nickel. Cada `ActionNode` declara: +- `trigger`: patrones de sujeto NATS que pueden activar el nodo como punto de entrada +- `input_schemas`: capacidades requeridas antes de la ejecución +- `output_schemas`: capacidades producidas tras la ejecución +- `compensate`: script de rollback opcional para atomicidad Saga + +El grafo se construye recorriendo índices de productor/consumidor de capacidades. El ordenamiento topológico genera un plan de ejecución en etapas. **Añadir un nuevo flujo de trabajo implica añadir ficheros `.ncl` — el binario del orquestador no cambia nunca.** + +### 2. Orquestador Sin Estado — DB-First + +El proceso del orquestador no mantiene estado duradero. Cada escritura en `PipelineContext` va primero a SurrealDB y luego actualiza una caché en memoria (`DashMap`). En caso de caída, una instancia reiniciada reconstruye la caché desde la DB. Esto permite: + +- **Escala horizontal**: múltiples instancias del orquestador comparten una SurrealDB sin split-brain +- **Recuperación ante caídas**: los pipelines reanudan desde la última capacidad persistida, no desde el principio +- **Observabilidad**: el estado completo del pipeline es consultable en cualquier momento sin instrumentar el proceso + +### 3. Nickel como Única Fuente de Verdad — Sin Verdad Dual + +Los nodos de acción, los esquemas de capacidad y la configuración de arranque del orquestador se definen en Nickel. No existe ningún proceso de indexación separado ni copia de las definiciones de nodos en base de datos usada en tiempo de ejecución. El `ActionGraph` se construye en memoria desde ficheros `.ncl` al arrancar mediante `nickel export --format json`, y se mantiene vivo mediante un observador de ficheros (`notify`) para recarga en caliente. + +Esto elimina el problema de verdad dual: el fichero Nickel ES la definición. No existe riesgo de que un registro de base de datos diverja del fichero en disco. + +### 4. Modelo de Capacidades — Inversión de Dependencia en el Dominio de Ejecución + +Los nodos no dependen unos de otros directamente. Declaran las capacidades que producen y consumen. El motor de grafo resuelve las dependencias: + +``` +lint-crate → produce: linted-code +fmt-crate → produce: formatted-code +build-crate → consume: linted-code, formatted-code + → produce: built-artifact +install-crate → consume: built-artifact +``` + +Esto es Inversión de Dependencia aplicada al dominio de ejecución: `build-crate` no conoce `lint-crate`. Solo sabe que necesita `linted-code`. Cualquier nodo que produzca `linted-code` satisface la dependencia. Los nodos pueden intercambiarse, reemplazarse o paralelizarse sin modificar sus consumidores. + +### 5. Tres Planos de Autenticación Independientes + +Autenticación y autorización están separadas en tres planos independientes, no intercambiables: + +| Plano | Tecnología | Alcance | Qué controla | +|-------|-----------|--------|--------------| +| **Auth de publicador** | NATS NKeys (ed25519) | Transporte | Quién puede publicar eventos en sujetos `dev.>` | +| **Authz de flujo** | Políticas Cedar | Orquestador | Qué pipelines/nodos puede disparar un principal | +| **Credenciales de ejecución** | Vault (SecretumVault) | Por nodo, por paso | Secretos de alcance limitado con TTL = timeout del nodo | + +Las credenciales de Vault se inyectan como variables de entorno en el subproceso Nushell y se revocan si el nodo falla. Nunca aparecen en mensajes NATS, logs ni en `PipelineContext` (se redactan antes del almacenamiento). + +### 6. Atomicidad Saga — Compensación, no Transacciones + +Los pipelines se ejecutan hacia adelante a través de etapas. Si una etapa falla, el orquestador no deshace una transacción de base de datos — ejecuta scripts `compensate.nu` en orden inverso a través de todas las etapas previamente exitosas. Este es el patrón Saga: + +``` +Etapa 0: lint (ok) + fmt (ok) → ejecutada +Etapa 1: build (FALLO) → dispara compensación +Compensación etapa 0: → deshacer lint, deshacer fmt (en paralelo, inverso) +``` + +La compensación es de mejor esfuerzo: los fallos de compensación se registran pero no impiden que el pipeline alcance el estado `Compensated`. El registro en DB captura la traza completa de compensación. + +### 7. Etapas Paralelas con JoinSet + CancellationToken + +Dentro de cada etapa, los nodos sin dependencias de capacidad entre sí se ejecutan en paralelo usando `tokio::task::JoinSet`. El fail-fast se implementa mediante `CancellationToken`: el primer fallo de un nodo cancela el token, abortando todas las tareas hermanas de la etapa. + +``` +Etapa 0: [lint-crate ‖ fmt-crate] — paralelo (sin dependencia mutua) +Etapa 1: [build-crate] — secuencial (necesita ambas capacidades) +Etapa 2: [install-crate] — secuencial (necesita built-artifact) +``` + +### 8. OCI para Todo — Artefactos con Direccionamiento por Contenido + +Tanto las definiciones de nodos como la biblioteca base de Nickel se publican como artefactos OCI en un registro Zot. El pipeline de publicación para cada uno es: `nickel typecheck` → `gitleaks detect` → `nickel export` → `sha256sum` → `oras push` con anotaciones de hash de contenido. + +El binario `ncl-import-resolver` hace de puente entre OCI y el sistema de ficheros local: descarga cada capa OCI referenciada al arrancar el orquestador, verifica el digest contra el hash anotado y expone una ruta local para las importaciones de Nickel. Esto impide cargar definiciones de nodos no verificadas o manipuladas. + +Sigue el mismo modelo que las imágenes de contenedor: construir → escanear → publicar → consumir por digest. + +### 9. Nushell como Unidad de Ejecución — Agnóstico por Diseño + +El campo `handler` de cada nodo de acción apunta a un script Nushell. El executor lanza `nu --no-config-file `, pasa los inputs del `PipelineContext` como JSON en stdin y lee el JSON de salida desde stdout. Esto hace la ejecución: + +- **Agnóstica al dominio**: el orquestador no conoce qué hace el script +- **Reemplazable en caliente**: actualizar un flujo de trabajo implica reemplazar un fichero `.nu`, no recompilar el binario +- **Aislable**: cada nodo se ejecuta en su propio proceso con credenciales Vault de alcance limitado +- **Testeable de forma independiente**: los scripts pueden invocarse directamente con `echo '{}' | nu script.nu` + +### 10. Alcance de TypeDialog — Solo Config de Arranque + +TypeDialog se usa exclusivamente para la configuración de arranque del orquestador (URL de SurrealDB, NATS, Zot, Vault, nivel de log, flags de funcionalidad). **No** se usa para declarativas de proyectos, definiciones de flujos ni configuración de nodos. Estas viven en ficheros Nickel gestionados por cada proyecto. Esto evita que TypeDialog se convierta en una herramienta de configuración general y mantiene su alcance acotado. + +## Decisión + +`stratum-orchestrator` se implementa como una familia de crates nuevos en el monorepo StratumIOps: + +| Crate | Dominio | Responsabilidad | +|-------|---------|----------------| +| `stratum-graph` | Conocimiento | `ActionNode`, `Capability`, trait `GraphRepository` | +| `stratum-state` | Operacional | `PipelineRun`, `StepRecord`, trait `StateTracker` | +| `platform-nats` | Transporte | Consumidor JetStream con auth NKey | +| `stratum-orchestrator` | Coordinación | `ActionGraph`, `PipelineContext`, `StageRunner`, auth, executor | + +El aislamiento de dominios es estructural: `stratum-graph` y `stratum-state` son crates separados con namespaces de tabla SurrealDB separados. `stratum-orchestrator` depende de sus traits, no de sus implementaciones — cumplimiento en tiempo de compilación. + +Secuencia de arranque del binario: cargar config TypeDialog → conectar SurrealDB → conectar NATS → resolver importaciones Nickel OCI → construir ActionGraph → iniciar observador notify → inicializar políticas Cedar → iniciar servidor HTTP (health + callback agente) → entrar en bucle pull JetStream. + +## Justificación + +### ¿Por Qué No un Motor de Flujos de Propósito General (Temporal, Argo, etc.)? + +| Consideración | Motor externo | stratum-orchestrator | +|--------------|---------------|----------------------| +| Modelo de eventos multi-proyecto | Requiere adaptador por proyecto | Coincidencia nativa de sujetos NATS | +| Integración Nickel | No viable | Primera clase: los nodos son ficheros `.ncl` | +| Ejecución Nushell | No soportado | Executor nativo de subprocesos | +| Coste operacional | Pesado (cluster Temporal, Argo en K8s) | Binario único + SurrealDB + NATS | +| Modelo de auth personalizado | Difícil de extender | Tres planos diseñados desde el inicio | + +### ¿Por Qué Saga en vez de 2PC? + +El commit en dos fases entre scripts Nushell distribuidos no es viable: los scripts son procesos externos sin coordinador de transacciones. Los scripts de compensación Saga (`compensate.nu`) son el único modelo de atomicidad realista para flujos de trabajo multi-proceso. La concesión es asumida: la compensación es de mejor esfuerzo, no garantizadamente atómica, pero los casos de fallo son registrados y auditables. + +### ¿Por Qué ActionGraph en Memoria vs Nodos Persistidos en DB? + +Almacenar las definiciones de nodos en SurrealDB crea verdad dual. El fichero en disco y el registro en DB pueden divergir. La recarga en caliente mediante `notify` sobre el sistema de ficheros es más simple, más rápida y elimina el problema de sincronización. SurrealDB solo se usa para el estado operacional (ejecuciones de pipeline, almacenes de capacidades) — el conocimiento (definiciones de nodos) permanece en el sistema de ficheros. + +## Consecuencias + +**Concesiones asumidas**: +- `nickel export` es una llamada a subproceso por fichero al arrancar — añade ~50ms por fichero de nodo al tiempo de arranque. Mitigado con carga paralela mediante `JoinSet` durante el arranque. +- La compensación Saga es de mejor esfuerzo — un script de compensación que falla se registra pero no bloquea la progresión de estado. Es una concesión conocida del patrón Saga. +- Coste de subproceso Nushell por nodo — cada ejecución de nodo lanza un proceso. Para scripts sub-segundo esto es latencia observable. Aceptable para flujos CI/CD e infraestructura. +- Descarga de capa OCI al arrancar — los arranques en frío requieren descargar capas de la biblioteca Nickel. Mitigado con caché local de digest en `~/.cache/stratum/ncl/`. + +**Beneficios obtenidos**: +- Nuevos flujos de trabajo solo requieren nuevos ficheros `.ncl` — cero cambios en el binario del orquestador +- Traza de auditoría completa del pipeline en SurrealDB: cada paso, cada depósito de capacidad, cada compensación +- La recuperación ante caídas es gratuita: reiniciar el orquestador, el pipeline reanuda desde el último estado persistido +- La autenticación no es negociable: identidad del publicador (NKeys), autorización del flujo (Cedar) y credenciales de ejecución (Vault) se aplican en cada invocación de pipeline +- Escalado horizontal: orquestador sin estado + SurrealDB compartida permite múltiples instancias en el mismo stream de eventos + +## Referencias + +- Plan de implementación: `.coder/2026-02-20-stratum-orchestrator-plan.plan.md` +- Diagrama de arquitectura: `assets/diagrams/arch-stratum-orchestrator.svg` +- Flujo de pipeline de build: `assets/diagrams/flow-stratum-build-pipeline.svg` +- Biblioteca base Nickel: `nickel/stratum-base/stratum-base.ncl` +- Crates: `crates/stratum-graph/`, `crates/stratum-state/`, `crates/platform-nats/`, `crates/stratum-orchestrator/` +- Relacionados: ADR-001 (stratum-embeddings), ADR-002 (stratum-llm) diff --git a/docs/es/architecture/adrs/README.md b/docs/es/architecture/adrs/README.md index 6cef791..9630b79 100644 --- a/docs/es/architecture/adrs/README.md +++ b/docs/es/architecture/adrs/README.md @@ -4,10 +4,11 @@ Registro de decisiones arquitecturales del ecosistema STRATUMIOPS. ## ADRs Activos -| ID | Título | Estado | -| -------------------------------- | ------------------------------------------------------ | --------- | -| [001](001-stratum-embeddings.md) | Stratum-Embeddings: Biblioteca Unificada de Embeddings | Propuesto | -| [002](002-stratum-llm.md) | Stratum-LLM: Biblioteca Unificada de Providers LLM | Propuesto | +| ID | Título | Estado | +| ---------------------------------------- | --------------------------------------------------------------- | --------- | +| [001](001-stratum-embeddings.md) | Stratum-Embeddings: Biblioteca Unificada de Embeddings | Propuesto | +| [002](002-stratum-llm.md) | Stratum-LLM: Biblioteca Unificada de Providers LLM | Propuesto | +| [003](003-stratum-orchestrator.md) | Stratum-Orchestrator: Orquestador de Flujos Guiado por Grafo | Aceptado | ## Estados diff --git a/nickel/stratum-base/stratum-base.ncl b/nickel/stratum-base/stratum-base.ncl new file mode 100644 index 0000000..f98b497 --- /dev/null +++ b/nickel/stratum-base/stratum-base.ncl @@ -0,0 +1,60 @@ +# stratum-base.ncl — shared Nickel types for stratum action nodes. +# Published as OCI artifact to Zot registry. +# Import: let base = import "nickel/stratum-base/stratum-base.ncl" in + +let RetryPolicy = { + max | Number | default = 3, + backoff_secs | Number | default = 10, + strategy | [| 'fixed, 'linear, 'exponential |] | default = 'exponential, +} +in + +let default_retry = { + max = 3, + backoff_secs = 10, + strategy = 'exponential, +} +in + +let NodeDefinition = { + id | String, + handler | String, + input_schemas | { _ : String } | default = {}, + output_schemas | { _ : String } | default = {}, + compensate | std.option.Option String | default = std.option.None, + retry | RetryPolicy | default = default_retry, + timeout_secs | Number | default = 300, + atomic | Bool | default = true, + triggers | Array String | default = [], +} +in + +let CapabilitySchema = { + name | String, + description | String, + schema | { _ : Dyn }, +} +in + +let SagaConfig = { + compensation_order | [| 'reverse, 'parallel |] | default = 'reverse, + fail_fast | Bool | default = true, +} +in + +let ActionGroup = { + id | String, + nodes | Array NodeDefinition, + trigger | Array String, + saga | SagaConfig | default = {}, +} +in + +{ + NodeDefinition, + RetryPolicy, + CapabilitySchema, + SagaConfig, + ActionGroup, + default_retry, +} diff --git a/schemas/capabilities/built-artifact.ncl b/schemas/capabilities/built-artifact.ncl new file mode 100644 index 0000000..1ee5b4f --- /dev/null +++ b/schemas/capabilities/built-artifact.ncl @@ -0,0 +1,11 @@ +{ + "$schema" = "http://json-schema.org/draft-07/schema#", + title = "built-artifact", + type = "object", + required = ["path", "crate_name", "ok"], + properties = { + path = { type = "string" }, + crate_name = { type = "string" }, + ok = { type = "boolean" }, + }, +} diff --git a/schemas/capabilities/formatted-code.ncl b/schemas/capabilities/formatted-code.ncl new file mode 100644 index 0000000..c880afb --- /dev/null +++ b/schemas/capabilities/formatted-code.ncl @@ -0,0 +1,10 @@ +{ + "$schema" = "http://json-schema.org/draft-07/schema#", + title = "formatted-code", + type = "object", + required = ["ok"], + properties = { + ok = { type = "boolean" }, + changed = { type = "integer" }, + }, +} diff --git a/schemas/capabilities/installed.ncl b/schemas/capabilities/installed.ncl new file mode 100644 index 0000000..9eae12c --- /dev/null +++ b/schemas/capabilities/installed.ncl @@ -0,0 +1,11 @@ +{ + "$schema" = "http://json-schema.org/draft-07/schema#", + title = "installed", + type = "object", + required = ["binary_path", "version", "ok"], + properties = { + binary_path = { type = "string" }, + version = { type = "string" }, + ok = { type = "boolean" }, + }, +} diff --git a/schemas/capabilities/linted-code.ncl b/schemas/capabilities/linted-code.ncl new file mode 100644 index 0000000..972919b --- /dev/null +++ b/schemas/capabilities/linted-code.ncl @@ -0,0 +1,12 @@ +{ + "$schema" = "http://json-schema.org/draft-07/schema#", + title = "linted-code", + description = "Output of lint-crate node", + type = "object", + required = ["warnings", "errors", "ok"], + properties = { + warnings = { type = "integer" }, + errors = { type = "integer" }, + ok = { type = "boolean" }, + }, +} diff --git a/scripts/nu/build-rollback.nu b/scripts/nu/build-rollback.nu new file mode 100644 index 0000000..9cf2e10 --- /dev/null +++ b/scripts/nu/build-rollback.nu @@ -0,0 +1,15 @@ +#!/usr/bin/env nu +# build-rollback.nu — compensation for build-crate. +# Removes build artifacts for the failed pipeline run. + +def main []: nothing -> nothing { + let run_id = ($env | get --ignore-errors PIPELINE_RUN_ID | default "unknown") + print $"Compensating build for pipeline run [$run_id]" + + let clean = (do { ^cargo clean } | complete) + if ($clean.exit_code != 0) { + print $"WARNING: cargo clean failed: ($clean.stderr)" + } else { + print "Build artifacts cleaned" + } +} diff --git a/scripts/nu/build.nu b/scripts/nu/build.nu new file mode 100644 index 0000000..75cc2e9 --- /dev/null +++ b/scripts/nu/build.nu @@ -0,0 +1,34 @@ +#!/usr/bin/env nu +# build.nu — run cargo build --release. +# Reads JSON inputs from stdin (linted-code, formatted-code). +# Emits JSON result to stdout. + +def main []: nothing -> nothing { + let inputs = ($in | from json) + + # Verify upstream capabilities are present + let linted = ($inputs | get "linted-code") + let formatted = ($inputs | get "formatted-code") + + if (not ($linted.ok)) { + error make { msg: "linted-code.ok is false — refusing to build" } + } + if (not ($formatted.ok)) { + error make { msg: "formatted-code.ok is false — refusing to build" } + } + + let result = (do { ^cargo build --release } | complete) + if ($result.exit_code != 0) { + error make { msg: $"cargo build failed:\n($result.stderr)" } + } + + # Locate the produced binary (assumes single binary workspace) + let binary = ( + ^find target/release -maxdepth 1 -type f -perm /111 -not -name "*.d" -not -name "*.rlib" + | lines + | first + ) + let crate_name = ($binary | path basename) + + { "built-artifact": { ok: true, path: $binary, crate_name: $crate_name } } | to json | print +} diff --git a/scripts/nu/fmt.nu b/scripts/nu/fmt.nu new file mode 100644 index 0000000..4188826 --- /dev/null +++ b/scripts/nu/fmt.nu @@ -0,0 +1,16 @@ +#!/usr/bin/env nu +# fmt.nu — run cargo fmt --check on the crate. +# Reads JSON inputs from stdin. Emits JSON result to stdout. + +def main []: nothing -> nothing { + let inputs = ($in | from json) + let _ = $inputs # inputs acknowledged but fmt needs none + + let result = (do { ^cargo fmt -- --check } | complete) + let changed = if ($result.exit_code == 0) { 0 } else { 1 } + + let fix = (do { ^cargo fmt } | complete) + let ok = ($fix.exit_code == 0) + + { "formatted-code": { ok: $ok, changed: $changed } } | to json | print +} diff --git a/scripts/nu/install-rollback.nu b/scripts/nu/install-rollback.nu new file mode 100644 index 0000000..d5b32e4 --- /dev/null +++ b/scripts/nu/install-rollback.nu @@ -0,0 +1,27 @@ +#!/usr/bin/env nu +# install-rollback.nu — compensation for install-crate. +# Removes the installed binary if it was placed during a now-failed pipeline. + +def main []: nothing -> nothing { + let run_id = ($env | get --ignore-errors PIPELINE_RUN_ID | default "unknown") + print $"Compensating install for pipeline run [$run_id]" + + # The binary name must be deterministic — read from env or derive from cargo metadata + let crate_name = ($env | get --ignore-errors STRATUM_CRATE_NAME | default "") + if ($crate_name | is-empty) { + print "STRATUM_CRATE_NAME not set — skipping install rollback" + return + } + + let target = ($nu.home-dir | path join ".local" "bin" $crate_name) + if ($target | path exists) { + let rm_result = (do { ^rm $target } | complete) + if ($rm_result.exit_code != 0) { + print $"WARNING: failed to remove ($target): ($rm_result.stderr)" + } else { + print $"Removed ($target)" + } + } else { + print $"($target) not present — nothing to remove" + } +} diff --git a/scripts/nu/install.nu b/scripts/nu/install.nu new file mode 100644 index 0000000..008ffb7 --- /dev/null +++ b/scripts/nu/install.nu @@ -0,0 +1,35 @@ +#!/usr/bin/env nu +# install.nu — install a built artifact to ~/.local/bin. +# Reads JSON inputs from stdin (built-artifact). +# Emits JSON result to stdout. + +def main []: nothing -> nothing { + let inputs = ($in | from json) + let artifact = ($inputs | get "built-artifact") + + if (not ($artifact.ok)) { + error make { msg: "built-artifact.ok is false — refusing to install" } + } + + let src = $artifact.path + let target = ($nu.home-dir | path join ".local" "bin" $artifact.crate_name) + + let cp_result = (do { ^cp $src $target } | complete) + if ($cp_result.exit_code != 0) { + error make { msg: $"cp failed: ($cp_result.stderr)" } + } + + let chmod_result = (do { ^chmod +x $target } | complete) + if ($chmod_result.exit_code != 0) { + error make { msg: $"chmod failed: ($chmod_result.stderr)" } + } + + let version_result = (do { nu -c $"($target) --version" } | complete) + let version = if ($version_result.exit_code == 0) { + $version_result.stdout | str trim + } else { + "unknown" + } + + { "installed": { ok: true, binary_path: $target, version: $version } } | to json | print +} diff --git a/scripts/nu/lint.nu b/scripts/nu/lint.nu new file mode 100644 index 0000000..44e2b24 --- /dev/null +++ b/scripts/nu/lint.nu @@ -0,0 +1,16 @@ +#!/usr/bin/env nu +# lint.nu — run cargo clippy on the crate in the current directory. +# Reads JSON inputs from stdin. Emits JSON result to stdout. + +def main []: nothing -> nothing { + let inputs = ($in | from json) + let crate_path = ($inputs | get --ignore-errors crate_path | default ".") + + let result = (do { ^cargo clippy --all-features -- -D warnings } | complete) + + let ok = ($result.exit_code == 0) + let warnings = if $ok { 0 } else { 1 } + let errors = if $ok { 0 } else { 1 } + + { "linted-code": { ok: $ok, warnings: $warnings, errors: $errors } } | to json | print +} diff --git a/scripts/nu/notify.nu b/scripts/nu/notify.nu new file mode 100644 index 0000000..bdf06d5 --- /dev/null +++ b/scripts/nu/notify.nu @@ -0,0 +1,26 @@ +#!/usr/bin/env nu +# notify.nu — emit a NATS result event after successful install. +# Reads JSON inputs from stdin (installed). +# Emits empty JSON object to stdout (no output capabilities). + +def main []: nothing -> nothing { + let inputs = ($in | from json) + let installed = ($inputs | get "installed") + + if (not ($installed.ok)) { + error make { msg: "installed.ok is false — not notifying" } + } + + let run_id = ($env | get --ignore-errors PIPELINE_RUN_ID | default "unknown") + let payload = { run_id: $run_id, binary: $installed.binary_path, version: $installed.version } + + let pub_result = (do { + ^nats pub dev.crate.built ($payload | to json) + } | complete) + + if ($pub_result.exit_code != 0) { + error make { msg: $"nats pub failed: ($pub_result.stderr)" } + } + + {} | to json | print +} diff --git a/scripts/nu/publish-ncl-lib.nu b/scripts/nu/publish-ncl-lib.nu new file mode 100644 index 0000000..92b39c4 --- /dev/null +++ b/scripts/nu/publish-ncl-lib.nu @@ -0,0 +1,68 @@ +#!/usr/bin/env nu +# publish-ncl-lib.nu — Publish a Nickel library as an OCI artifact to Zot registry. +# Usage: nu publish-ncl-lib.nu --lib-dir --registry --name --tag + +def main [ + --lib-dir: string, # directory containing the .ncl file + --registry: string, # OCI registry URL e.g. localhost:5000 + --name: string, # library name e.g. stratum-base + --tag: string, # version tag e.g. 0.1.0 +]: nothing -> nothing { + print $"Publishing Nickel lib [$name]:[$tag] to [$registry]" + + let ncl_file = ($lib_dir | path join $"($name).ncl") + + # Step 1: typecheck + let check = (do { ^nickel typecheck $ncl_file } | complete) + if ($check.exit_code != 0) { + error make { msg: $"nickel typecheck failed:\n($check.stderr)" } + } + print "typecheck passed" + + # Step 2: gitleaks secret scan + let leak = (do { ^gitleaks detect --source $lib_dir --no-git } | complete) + if ($leak.exit_code != 0) { + error make { msg: $"gitleaks detected secrets in [$lib_dir]:\n($leak.stdout)" } + } + print "gitleaks clean" + + # Step 3: export JSON artifact + let export_path = ($nu.temp-dir | path join $"($name)-export.json") + let export = (do { ^nickel export --format json $ncl_file } | complete) + if ($export.exit_code != 0) { + error make { msg: $"nickel export failed:\n($export.stderr)" } + } + $export.stdout | save --force $export_path + print $"nickel export → [$export_path]" + + # Step 4: sha256 + let sha_result = (do { ^sha256sum $export_path } | complete) + if ($sha_result.exit_code != 0) { + error make { msg: $"sha256sum failed: ($sha_result.stderr)" } + } + let sha = ($sha_result.stdout | split row " " | first) + print $"sha256: [$sha]" + + # Step 5: oras push + let image_ref = $"($registry)/($name):($tag)" + let nickel_ver_result = (do { ^nickel --version } | complete) + let nickel_ver = if ($nickel_ver_result.exit_code == 0) { + $nickel_ver_result.stdout | str trim + } else { + "unknown" + } + + let push = (do { + ^oras push $image_ref + $"($export_path):application/vnd.stratumiops.ncl.export.v1+json" + --annotation $"org.stratumiops.lib.name=($name)" + --annotation $"org.stratumiops.lib.sha256=($sha)" + --annotation $"org.stratumiops.lib.tag=($tag)" + --annotation $"org.stratumiops.lib.nickel-version=($nickel_ver)" + } | complete) + + if ($push.exit_code != 0) { + error make { msg: $"oras push failed:\n($push.stderr)" } + } + print $"pushed [$image_ref]" +} diff --git a/scripts/nu/start-dev-stack.nu b/scripts/nu/start-dev-stack.nu new file mode 100644 index 0000000..62fb538 --- /dev/null +++ b/scripts/nu/start-dev-stack.nu @@ -0,0 +1,39 @@ +#!/usr/bin/env nu +# start-dev-stack.nu — Bring up the stratum docker-compose dev stack and wait for health. + +def wait-for-http [url: string, label: string, max_secs: int]: nothing -> nothing { + let start = (date now) + loop { + let elapsed = ((date now) - $start | into int) / 1_000_000_000 + if ($elapsed > $max_secs) { + error make { msg: $"[$label] not ready after [$max_secs]s" } + } + let r = (do { ^curl -sf $url } | complete) + if ($r.exit_code == 0) { + print $"[$label] ready" + break + } + sleep 2sec + } +} + +def main []: nothing -> nothing { + print "Starting stratum dev stack..." + + let up = (do { + ^docker compose -f docker-compose.dev.yml up -d + } | complete) + + if ($up.exit_code != 0) { + error make { msg: $"docker compose up failed:\n($up.stderr)" } + } + + print "Containers started, waiting for services..." + + wait-for-http "http://localhost:8100/health" "SurrealDB" 30 + wait-for-http "http://localhost:5000/v2/" "Zot OCI" 30 + + print "Dev stack ready" + print " SurrealDB: ws://localhost:8100" + print " Zot OCI: http://localhost:5000" +} diff --git a/scripts/nu/start-orchestrator.nu b/scripts/nu/start-orchestrator.nu new file mode 100644 index 0000000..35811fa --- /dev/null +++ b/scripts/nu/start-orchestrator.nu @@ -0,0 +1,34 @@ +#!/usr/bin/env nu +# start-orchestrator.nu — Start the stratum orchestrator with full pre-flight checks. + +def main [ + --config: string = "config/orchestrator-config.ncl", + --skip-typecheck = false, +]: nothing -> nothing { + print "Starting orchestrator..." + + # 1. Typecheck all .ncl files + if (not $skip_typecheck) { + print "Running Nickel typecheck..." + let tc = (do { ^nu scripts/nu/typecheck-all-ncl.nu } | complete) + if ($tc.exit_code != 0) { + error make { msg: $"Nickel typecheck failed — fix errors before starting orchestrator:\n($tc.stderr)" } + } + print "Nickel typecheck passed" + } + + # 2. Verify config file exists and typechecks + if (not ($config | path exists)) { + error make { msg: $"Config file not found: [$config]" } + } + + let cfg_check = (do { ^nickel typecheck $config } | complete) + if ($cfg_check.exit_code != 0) { + error make { msg: $"Orchestrator config failed typecheck:\n($cfg_check.stderr)" } + } + print $"Config typechecked: [$config]" + + # 3. Exec orchestrator binary + print "Launching orchestrator..." + ^./target/debug/orchestrator --config $config +} diff --git a/scripts/nu/typecheck-all-ncl.nu b/scripts/nu/typecheck-all-ncl.nu new file mode 100644 index 0000000..ad9f167 --- /dev/null +++ b/scripts/nu/typecheck-all-ncl.nu @@ -0,0 +1,24 @@ +#!/usr/bin/env nu +# typecheck-all-ncl.nu — Run nickel typecheck on all .ncl files. Exit 1 on any failure. + +def main []: nothing -> nothing { + let all_files = (glob "**/*.ncl" | where { |f| + not ($f | str contains "/.git/") + }) + + let results = ($all_files | each { |f| + let r = (do { ^nickel typecheck $f } | complete) + { file: $f, ok: ($r.exit_code == 0), err: $r.stderr } + }) + + let failures = ($results | where { |r| not $r.ok }) + + if (($failures | length) > 0) { + $failures | each { |f| + print $"FAIL ($f.file):\n($f.err)" + } + error make { msg: $"($failures | length) Nickel typecheck failures" } + } + + print $"($results | length) .ncl files typechecked" +} diff --git a/workspaces/orchestrator-dev.ncl b/workspaces/orchestrator-dev.ncl new file mode 100644 index 0000000..b596000 --- /dev/null +++ b/workspaces/orchestrator-dev.ncl @@ -0,0 +1,12 @@ +{ + name = "orchestrator-dev", + services = [ + { + name = "stratum-orchestrator", + binary = "target/debug/orchestrator", + config = "config/orchestrator-config.ncl", + depends_on = ["surrealdb-stratum", "nats"], + } + ], + external_services = ["surrealdb-stratum", "zot-stratum", "nats"], +}