From 175374fd2105d2ae7ae3050ebeda8b5f43eceab5 Mon Sep 17 00:00:00 2001 From: forhappy Date: Sun, 4 Oct 2026 14:55:31 -0700 Subject: [PATCH 1/7] docs(examples): add end-to-end application builder and Axum prototype --- cookbook/support/Cargo.toml | 1 + .../application-builder-service/Cargo.lock | 3005 +++++++++++++++++ .../application-builder-service/Cargo.toml | 30 + .../application-builder-service/README.md | 167 + .../src/application.rs | 138 + .../application-builder-service/src/auth.rs | 116 + .../src/journal.rs | 113 + .../application-builder-service/src/main.rs | 165 + .../src/proposal.rs | 169 + .../src/recovery.rs | 91 + 10 files changed, 3995 insertions(+) create mode 100644 examples/application-builder-service/Cargo.lock create mode 100644 examples/application-builder-service/Cargo.toml create mode 100644 examples/application-builder-service/README.md create mode 100644 examples/application-builder-service/src/application.rs create mode 100644 examples/application-builder-service/src/auth.rs create mode 100644 examples/application-builder-service/src/journal.rs create mode 100644 examples/application-builder-service/src/main.rs create mode 100644 examples/application-builder-service/src/proposal.rs create mode 100644 examples/application-builder-service/src/recovery.rs diff --git a/cookbook/support/Cargo.toml b/cookbook/support/Cargo.toml index 42f63343..50670f91 100644 --- a/cookbook/support/Cargo.toml +++ b/cookbook/support/Cargo.toml @@ -1,4 +1,5 @@ [package] +workspace = ".." name = "cellule-cookbook-support" version.workspace = true edition.workspace = true diff --git a/examples/application-builder-service/Cargo.lock b/examples/application-builder-service/Cargo.lock new file mode 100644 index 00000000..9fb496f2 --- /dev/null +++ b/examples/application-builder-service/Cargo.lock @@ -0,0 +1,3005 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cellule-app" +version = "0.1.0" +dependencies = [ + "blake3", + "cellule-runtime", +] + +[[package]] +name = "cellule-axum" +version = "0.1.0" +dependencies = [ + "axum", + "cellule-app", + "cellule-runtime", + "serde", + "serde_json", + "utoipa", + "utoipa-axum", + "uuid", +] + +[[package]] +name = "cellule-builder-service-example" +version = "0.1.0" +dependencies = [ + "axum", + "blake3", + "cellule-app", + "cellule-axum", + "cellule-cookbook-support", + "cellule-runtime", + "cellule-store", + "object_store", + "rusqlite", + "serde_json", + "tempfile", + "tokio", + "uuid", +] + +[[package]] +name = "cellule-cookbook-support" +version = "0.1.0" +dependencies = [ + "blake3", + "cellule-app", + "cellule-host", + "cellule-ltx", + "cellule-runtime", + "cellule-store", + "ed25519-dalek", + "object_store", + "rand 0.9.5", + "thiserror", + "tokio", + "tokio-util", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "cellule-host" +version = "0.1.0" +dependencies = [ + "blake3", + "cellule-app", + "cellule-runtime", + "futures-util", + "tokio", + "tokio-util", + "tracing", + "uuid", +] + +[[package]] +name = "cellule-ltx" +version = "0.1.0" +dependencies = [ + "async-trait", + "blake3", + "bytes", + "cellule-store", + "crc-fast", + "futures-util", + "lz4_flex", + "object_store", + "rusqlite", + "serde", + "serde_json", + "tempfile", + "thiserror", + "tokio", + "tokio-util", +] + +[[package]] +name = "cellule-runtime" +version = "0.1.0" +dependencies = [ + "blake3", + "bytes", + "cellule-ltx", + "cellule-store", + "ed25519-dalek", + "futures-util", + "object_store", + "prost", + "prost-build", + "protoc-bin-vendored", + "rand 0.9.5", + "rusqlite", + "serde", + "serde_json", + "tempfile", + "thiserror", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "cellule-store" +version = "0.1.0" +dependencies = [ + "async-trait", + "blake3", + "bytes", + "cellule-types", + "futures-util", + "hyper", + "object_store", + "rand 0.9.5", + "reqwest 0.12.28", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "cellule-types" +version = "0.1.0" +dependencies = [ + "schemars", + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc-fast" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" +dependencies = [ + "digest 0.10.7", + "spin", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "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.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[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", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libsqlite3-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "lz4_flex" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object_store" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1796bc93603f78c5760a69f2d58badc9618d22adade0a95385bb2adbae4eb94" +dependencies = [ + "async-trait", + "aws-lc-rs", + "base64 0.23.1", + "bytes", + "chrono", + "crc-fast", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body-util", + "httparse", + "humantime", + "hyper", + "itertools 0.15.0", + "md-5", + "nix", + "parking_lot", + "percent-encoding", + "quick-xml", + "rand 0.10.3", + "reqwest 0.13.5", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "thiserror", + "tokio", + "tracing", + "url", + "walkdir", + "wasm-bindgen-futures", + "web-time", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.119", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[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_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +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", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "twox-hash" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utoipa" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8765fe27aeff71012a3f90fa474cf1df863a7d0dd81ed25de4e63fff2544994f" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-axum" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865319162dc3d0032e1e40ed11bd49d5b78e8ef576740589fd6823e2e9977423" +dependencies = [ + "axum", + "pastey", + "tower-layer", + "tower-service", + "utoipa", +] + +[[package]] +name = "utoipa-gen" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d935f1c83fdf8b88f09bbe050d0cea78ea4afee6d7b0317403228c1200b2c8ab" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 3.0.6", + "uuid", +] + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "zerocopy" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure 0.14.0", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/application-builder-service/Cargo.toml b/examples/application-builder-service/Cargo.toml new file mode 100644 index 00000000..2f20882a --- /dev/null +++ b/examples/application-builder-service/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "cellule-builder-service-example" +version = "0.1.0" +edition = "2024" +rust-version = "1.97" +publish = false + +# This is an embedding application, independent of the framework workspace. +[workspace] +resolver = "3" + +[dependencies] +axum = { version = "0.8.9", features = ["json", "http1", "tokio"] } +blake3 = "1.8" +cellule-app = { path = "../../crates/cellule-app" } +cellule-axum = { path = "../../crates/cellule-axum", features = ["openapi"] } +cellule-cookbook-support = { path = "../../cookbook/support" } +cellule-runtime = { path = "../../crates/cellule-runtime" } +cellule-store = { path = "../../crates/cellule-store" } +object_store = { version = "0.14.1", default-features = false } +rusqlite = { version = "0.34", features = ["bundled"] } +serde_json = "1" +tempfile = "3" +tokio = { version = "1.49", features = ["macros", "net", "rt-multi-thread", "signal"] } +uuid = { version = "=1.24.0", features = ["v7"] } + +[lints.clippy] +unwrap_used = "deny" +expect_used = "deny" +panic = "deny" diff --git a/examples/application-builder-service/README.md b/examples/application-builder-service/README.md new file mode 100644 index 00000000..a7fd31a0 --- /dev/null +++ b/examples/application-builder-service/README.md @@ -0,0 +1,167 @@ +# Application builder → Axum service + +A complete embedding application illustrating the proposed builder ergonomics. +Run it against the current Cellule checkout; no framework API implementation is +assumed. The conveniences are implemented locally in [proposal.rs](src/proposal.rs) +and delegate to the existing application compiler and cookbook host assembly. + +```text +Orders module + Cell binding + │ + ▼ +OrdersApp::compile ───────────────► CellApi + OpenAPI + │ │ + ▼ ▼ +ServiceNode::start Axum routes + probe storage │ + enroll + renew lease │ + provision both tenant Cells │ + │ │ + └── scoped handle ◄── authenticate + authorize + +Shutdown: stop HTTP → finish requests → drain node → withdraw enrollment +``` + +## What is implemented + +| File | Responsibility | +| --- | --- | +| [application.rs](src/application.rs) | Declare the SQL module, migration, command/query contracts, and application binding. | +| [proposal.rs](src/proposal.rs) | Prototype `builder.module`, explicit fixed-shard `CellBinding`, node startup, handle factory, and shutdown. | +| [auth.rs](src/auth.rs) | Authenticate a credential, authorize read/write access, then derive a scoped handle and target. | +| [journal.rs](src/journal.rs) | Atomically retain the prepared snapshot and exact encoded input before dispatch. | +| [recovery.rs](src/recovery.rs) | Resolve original evidence; replay only after authoritative absence. | +| [main.rs](src/main.rs) | Compile once, start the node, compose Axum/OpenAPI, and drain on signals or serving errors. | + +`CellBinding` derives role, shard count, and schema range from the module +descriptor. Namespace identity, Cell name, limits, and fixed-shard partition +selection remain explicit. `ApplicationBuilder::finish` still performs the +canonical whole-application validation. Treat any registration error as a +failed compilation; this helper does not roll back a partially mutated builder. + +`ServiceNode` wraps the cookbook's `LocalNode`, which owns one `CellNode` and +one runtime. The host owns directory enrollment, lease renewal, supervision, +recovery, and ordered drain. The service still owns its storage provider, +application identity, authorization, HTTP routes, listener, and evidence journal. + +## Run + +From the repository root: + +```sh +cargo run --manifest-path examples/application-builder-service/Cargo.toml --locked +``` + +The server binds `127.0.0.1:3002`. Set `CELLULE_EXAMPLE_BIND=127.0.0.1:0` +to have the OS choose a free port; the selected address is printed. + +On a workstation with the mounted Workspace volume, set `CARGO_TARGET_DIR` +to a directory unique to this checkout beneath `$HOME/Workspace/crabbuild-target`. + +This tutorial uses an in-memory authoritative store, temporary local SQLite +files, and fixed local credentials. Restarting creates fresh state. Durable +publication here is relative to that provider's lifetime. For persistent +service state, supply a durable, successfully probed `Store`, stable application +identity, persistent state/journal directories, and the service's actual +identity and authorization implementation. The cookbook host is a single-node +recipe supporting schema version one; this prototype is not the general +production host startup API proposed for `cellule-host`. + +## Call the service + +| Credential | Tenant | Permission | +| --- | --- | --- | +| `Bearer alpha-writer` | Alpha | Read, write, resolve/replay its own command evidence | +| `Bearer beta-reader` | Beta | Read only | + +The authenticated server-side record chooses the tenant. Request headers, +path parameters, and mutation inputs cannot select another tenant. + +| Method and route | Body | Result | +| --- | --- | --- | +| `POST /orders/total` | Mutation identity + integer input | Committed total and receipt | +| `POST /orders/total/read` | JSON `null` | Observed total and receipt; accepts `x-cellule-receipt` | +| `POST /orders/total/resolve/{request_id}` | Empty | Original authorized command outcome, safe replay, or unresolved state | +| `GET /ready` | Empty | `204` when the node/lease are ready | +| `GET /openapi.json` | Empty | Generated command/query schemas, recovery/readiness routes, and bearer security | + +Write Alpha's total, retain the original body, then read at its receipt: + +```sh +set -eu +base=http://127.0.0.1:3002 +request_id=$(python3 -c 'import uuid; print(uuid.uuid4())') +now_ms=$(python3 -c 'import time; print(time.time_ns() // 1000000)') +body=$(printf '{"identity":{"request_id":"%s","issued_at_ms":%s,"expires_at_ms":%s},"input":1999}' "$request_id" "$now_ms" "$((now_ms + 60000))") + +reply=$(curl --fail-with-body --silent --show-error "$base/orders/total" \ + -H 'Authorization: Bearer alpha-writer' \ + -H 'Content-Type: application/json' --data "$body") +printf '%s\n' "$reply" + +receipt=$(printf '%s' "$reply" | python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin)["receipt"], separators=(",", ":")))') +curl --fail-with-body --silent --show-error "$base/orders/total/read" \ + -H 'Authorization: Bearer alpha-writer' \ + -H 'Content-Type: application/json' \ + -H "x-cellule-receipt: $receipt" --data 'null' + +# Exact retry returns the original outcome and receipt. +curl --fail-with-body --silent --show-error "$base/orders/total" \ + -H 'Authorization: Bearer alpha-writer' \ + -H 'Content-Type: application/json' --data "$body" + +# Resolve original retained evidence if the HTTP response was lost. +curl --fail-with-body --silent --show-error --request POST \ + "$base/orders/total/resolve/$request_id" \ + -H 'Authorization: Bearer alpha-writer' + +# Beta reads its independent total: output remains zero. +curl --fail-with-body --silent --show-error "$base/orders/total/read" \ + -H 'Authorization: Bearer beta-reader' \ + -H 'Content-Type: application/json' --data 'null' + +# This returns 403; the read-only caller cannot dispatch a command. +curl --silent --show-error --output /dev/null --write-out '%{http_code}\n' \ + "$base/orders/total" -H 'Authorization: Bearer beta-reader' \ + -H 'Content-Type: application/json' --data "$body" + +curl --fail-with-body --silent --show-error "$base/openapi.json" +``` + +The command's input/output is `i64`; the query accepts unit input (`null`). +Negative totals are durable domain rejections. Never create a new request ID +or change the original bytes merely because a response was lost. + +```mermaid +sequenceDiagram + actor Caller + participant HTTP as Axum + authorized extractor + participant Host as ServiceNode + participant Adapter as CellApi + participant Journal as Application journal + participant Runtime as Cellule runtime + Caller->>HTTP: POST identity + input + HTTP->>HTTP: Verify credential and write permission + HTTP->>Host: scope(authenticated tenant) + Host-->>HTTP: ApplicationHandle + authorized target + HTTP->>Adapter: Authorized context + mutation + Adapter->>Runtime: Prepare original command + Runtime-->>Adapter: Snapshot + exact input + evidence + Adapter->>Journal: Retain atomically + Journal-->>Adapter: Durable custody confirmed + Adapter->>Runtime: Execute + Runtime-->>Adapter: Outcome + receipt after durability gate + Adapter-->>Caller: CellJson + Note over HTTP,Runtime: On shutdown, finish accepted HTTP first + HTTP->>Host: shutdown() + Host->>Runtime: Drain with renewal still live + Runtime-->>Host: Accepted work and handles released + Host->>Host: Stop renewal and withdraw latest enrollment +``` + +Ctrl-C or SIGTERM stops HTTP admission and waits for accepted requests. Only +after `axum::serve` finishes does the node drain. Its lease-maintenance task +keeps renewal live during the drain and withdraws the latest enrollment last. +Route construction, listener bind, and serving failures take the same node +cleanup path. A failed drain reports its original error rather than claiming +successful withdrawal. diff --git a/examples/application-builder-service/src/application.rs b/examples/application-builder-service/src/application.rs new file mode 100644 index 00000000..94c449f1 --- /dev/null +++ b/examples/application-builder-service/src/application.rs @@ -0,0 +1,138 @@ +use std::sync::OnceLock; + +use cellule_app::{ApplicationBuilder, CellApplication}; +use cellule_runtime::{ + CatalogRole, CellModule, Command, Digest, MigrationDescriptor, ModuleDescriptor, + NamespaceDescriptor, NamespaceId, Query, RegistryBuilder, + primitives::sql::{SqlBatch, SqlStatement, SqlValue}, + registry::{CommandContext, CommandResult, OperationDescriptor, QueryContext}, +}; + +use crate::proposal::{ApplicationBuilderExt, CellBinding}; + +pub const ORDERS: NamespaceId = NamespaceId::from_bytes([23; 16]); +const SCHEMA: &str = "CREATE TABLE totals (id INTEGER PRIMARY KEY, cents INTEGER NOT NULL)"; +const COMMANDS: [OperationDescriptor; 1] = [operation(SetTotal::ID)]; +const QUERIES: [OperationDescriptor; 1] = [operation(ReadTotal::ID)]; + +pub struct Orders; +pub struct OrdersApp; +pub struct SetTotal; +pub struct ReadTotal; + +impl CellModule for Orders { + const NAME: &'static str = "example.orders"; + + fn descriptor(&self) -> &'static ModuleDescriptor { + static MIGRATIONS: OnceLock<[MigrationDescriptor; 1]> = OnceLock::new(); + static DESCRIPTOR: OnceLock = OnceLock::new(); + DESCRIPTOR.get_or_init(|| ModuleDescriptor { + name: Self::NAME, + source_digest: source_digest(), + retained_codes: &[], + schema_min: 1, + schema_max: 1, + migrations: MIGRATIONS.get_or_init(|| { + [MigrationDescriptor { + version: 1, + sql: SCHEMA, + digest: Digest::from_bytes(*blake3::hash(SCHEMA.as_bytes()).as_bytes()), + }] + }), + commands: &COMMANDS, + queries: &QUERIES, + workflow_definitions: &[], + activity_types: &[], + namespaces: &[NamespaceDescriptor { + id: ORDERS, + name: "orders", + role: CatalogRole::Sql, + shards: 1, + effect_targets: &[], + dead_letter: None, + }], + }) + } + + fn register(self, registry: &mut RegistryBuilder) -> cellule_runtime::Result<()> { + registry.bind_command::()?; + registry.bind_query::() + } +} + +impl CellApplication for OrdersApp { + const NAME: &'static str = "builder-service-example"; + + fn register(builder: &mut ApplicationBuilder) -> cellule_runtime::Result<()> { + // The helper reads role, shards and schema from Orders' descriptor. + // Stable namespace, Cell name, partition mode and limits stay explicit. + builder.module( + Orders, + [CellBinding::sharded(ORDERS, "orders").with_limits(64 << 20, 16 << 20)], + ) + } +} + +impl Command for SetTotal { + const MODULE: &'static str = Orders::NAME; + const ID: u32 = 1; + const CODEC_VERSION: u32 = 1; + type Input = i64; + type Output = i64; + + fn execute( + context: &mut CommandContext<'_, '_>, + cents: i64, + ) -> cellule_runtime::Result> { + if cents < 0 { + return Ok(CommandResult::Rejected(cents)); + } + context.sql(&SqlBatch { + statements: vec![SqlStatement { + sql: "INSERT INTO totals VALUES (1, ?1) ON CONFLICT(id) DO UPDATE SET cents=excluded.cents".into(), + parameters: vec![SqlValue::Integer(cents)], + }], + })?; + Ok(CommandResult::Success(cents)) + } +} + +impl Query for ReadTotal { + const MODULE: &'static str = Orders::NAME; + const ID: u32 = 2; + const CODEC_VERSION: u32 = 1; + type Input = (); + type Output = i64; + + fn execute(context: &mut QueryContext<'_>, (): ()) -> cellule_runtime::Result { + let results = context.sql(&SqlBatch { + statements: vec![SqlStatement { + sql: "SELECT cents FROM totals WHERE id=1".into(), + parameters: vec![], + }], + })?; + match results.first().map(|result| result.rows.as_slice()) { + Some([]) => Ok(0), + Some([row]) => match row.as_slice() { + [SqlValue::Integer(cents)] => Ok(*cents), + _ => Err(cellule_runtime::Error::Control("invalid total row")), + }, + _ => Err(cellule_runtime::Error::Control("invalid total result")), + } + } +} + +const fn operation(id: u32) -> OperationDescriptor { + OperationDescriptor { + id, + codec_version: 1, + schema_min: 1, + schema_max: 1, + input_limit: 1024, + output_limit: 1024, + } +} + +pub fn source_digest() -> Digest { + Digest::from_bytes(*blake3::hash(include_bytes!("application.rs")).as_bytes()) +} diff --git a/examples/application-builder-service/src/auth.rs b/examples/application-builder-service/src/auth.rs new file mode 100644 index 00000000..20d2d8aa --- /dev/null +++ b/examples/application-builder-service/src/auth.rs @@ -0,0 +1,116 @@ +use axum::{ + extract::{FromRequestParts, State}, + http::{HeaderMap, StatusCode, request::Parts}, + response::{IntoResponse, Response}, +}; +use cellule_app::ApplicationHandle; +use cellule_axum::{CellEndpoint, CommandEndpoint, HttpError, utoipa}; +use cellule_runtime::{CellTarget, PreparedCommand, TenantId}; +use std::sync::Arc; + +use crate::{ + ServiceState, + application::{ORDERS, OrdersApp, SetTotal}, + journal::Journal, +}; + +pub const ALPHA: TenantId = TenantId::from_bytes([1; 16]); +pub const BETA: TenantId = TenantId::from_bytes([2; 16]); + +struct Principal { + tenant: TenantId, + can_write: bool, +} + +// Loopback tutorial credentials map to server-owned tenant/permission records. +// Replace this function with your existing session/JWT verifier and ACL lookup. +fn authenticate(headers: &HeaderMap) -> Result { + let values = headers.get_all("authorization"); + if values.iter().count() != 1 { + return Err(StatusCode::UNAUTHORIZED); + } + match values.iter().next().and_then(|value| value.to_str().ok()) { + Some("Bearer alpha-writer") => Ok(Principal { + tenant: ALPHA, + can_write: true, + }), + Some("Bearer beta-reader") => Ok(Principal { + tenant: BETA, + can_write: false, + }), + _ => Err(StatusCode::UNAUTHORIZED), + } +} + +pub struct Authorized { + app: ApplicationHandle, + target: CellTarget, + journal: Journal, +} + +pub type WriteOrders = Authorized; +pub type ReadOrders = Authorized; + +impl Authorized { + pub fn journal(&self) -> &Journal { + &self.journal + } +} + +impl FromRequestParts> for Authorized { + type Rejection = Response; + + async fn from_request_parts( + parts: &mut Parts, + state: &Arc, + ) -> Result { + let principal = authenticate(&parts.headers).map_err(IntoResponse::into_response)?; + if WRITE && !principal.can_write { + return Err(StatusCode::FORBIDDEN.into_response()); + } + if !state.node.is_ready() { + return Err(StatusCode::SERVICE_UNAVAILABLE.into_response()); + } + // Client headers/body cannot replace the authenticated tenant or target. + let app = state + .node + .scope::(principal.tenant) + .map_err(|error| HttpError::internal(error).into_response())?; + let target = app + .target_for_scope(ORDERS, b"orders") + .map_err(|error| HttpError::from(error).into_response())?; + Ok(Self { + app, + target, + journal: state.journal.clone(), + }) + } +} + +impl CellEndpoint for Authorized { + fn application(&self) -> &ApplicationHandle { + &self.app + } + fn target(&self) -> &CellTarget { + &self.target + } +} + +impl CommandEndpoint for WriteOrders { + async fn retain(&self, prepared: &PreparedCommand) -> Result<(), HttpError> { + self.journal.retain(prepared).await + } +} + +#[cellule_axum::utoipa::path( + get, path = "/ready", operation_id = "readiness", + responses((status = 204, description = "Node and lease are ready"), + (status = 503, description = "Node is unavailable")) +)] +pub async fn ready(State(state): State>) -> StatusCode { + if state.node.is_ready() { + StatusCode::NO_CONTENT + } else { + StatusCode::SERVICE_UNAVAILABLE + } +} diff --git a/examples/application-builder-service/src/journal.rs b/examples/application-builder-service/src/journal.rs new file mode 100644 index 00000000..967cb4ab --- /dev/null +++ b/examples/application-builder-service/src/journal.rs @@ -0,0 +1,113 @@ +//! Application-owned atomic custody, separate from the Cell's authoritative ledger. + +use crate::application::SetTotal; +use cellule_axum::HttpError; +use cellule_runtime::{Error, PreparedCommand, PreparedCommandSnapshot}; +use rusqlite::{Connection, OptionalExtension, params}; +use std::{ + path::{Path, PathBuf}, + time::Duration, +}; +use uuid::Uuid; + +#[derive(Clone)] +pub struct Journal(PathBuf); +type EncodedRecord = (Vec, Vec); + +fn connection(path: &Path) -> Result { + let connection = Connection::open(path)?; + connection.busy_timeout(Duration::from_secs(5))?; + connection.execute_batch("PRAGMA synchronous=FULL;")?; + Ok(connection) +} + +impl Journal { + pub fn open(path: PathBuf) -> Result { + let connection = connection(&path)?; + connection.execute_batch( + "PRAGMA journal_mode=WAL; + CREATE TABLE IF NOT EXISTS evidence ( + cell BLOB NOT NULL, request BLOB NOT NULL, + snapshot BLOB NOT NULL, input BLOB NOT NULL, + PRIMARY KEY(cell, request) + );", + )?; + Ok(Self(path)) + } + + pub async fn retain(&self, prepared: &PreparedCommand) -> Result<(), HttpError> { + let path = self.0.clone(); + let cell = prepared.evidence().target().cell_id().as_bytes().to_vec(); + let request = prepared + .evidence() + .identity() + .request_id + .as_bytes() + .to_vec(); + let snapshot = prepared + .snapshot() + .to_bytes() + .map_err(HttpError::internal)?; + let input = prepared.input_bytes().to_vec(); + tokio::task::spawn_blocking(move || -> Result<(), Error> { + let mut connection = connection(&path)?; + let transaction = + connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?; + let existing: Option = transaction + .query_row( + "SELECT snapshot, input FROM evidence WHERE cell=?1 AND request=?2", + params![cell, request], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional()?; + match existing { + Some((old_snapshot, old_input)) + if old_snapshot != snapshot || old_input != input => + { + return Err(Error::RequestConflict); + } + Some(_) => (), + None => { + transaction.execute( + "INSERT INTO evidence VALUES (?1, ?2, ?3, ?4)", + params![cell, request, snapshot, input], + )?; + } + } + // Header and exact encoded input become durable together, before dispatch. + transaction.commit()?; + Ok(()) + }) + .await + .map_err(HttpError::internal)? + .map_err(HttpError::from) + } + + pub async fn load( + &self, + cell: [u8; 32], + request: Uuid, + ) -> Result)>, HttpError> { + let path = self.0.clone(); + let record = + tokio::task::spawn_blocking(move || -> Result, Error> { + Ok(connection(&path)? + .query_row( + "SELECT snapshot, input FROM evidence WHERE cell=?1 AND request=?2", + params![cell.as_slice(), request.as_bytes().as_slice()], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional()?) + }) + .await + .map_err(HttpError::internal)??; + record + .map(|(header, input)| { + Ok(( + PreparedCommandSnapshot::from_bytes(&header).map_err(HttpError::internal)?, + input, + )) + }) + .transpose() + } +} diff --git a/examples/application-builder-service/src/main.rs b/examples/application-builder-service/src/main.rs new file mode 100644 index 00000000..455d9162 --- /dev/null +++ b/examples/application-builder-service/src/main.rs @@ -0,0 +1,165 @@ +mod application; +mod auth; +mod journal; +mod proposal; +mod recovery; + +use std::sync::Arc; + +use axum::{ + Json, Router, + extract::DefaultBodyLimit, + routing::{get, post}, +}; +use cellule_app::CellApplication; +use cellule_axum::{CellApi, EndpointSpec, utoipa}; +use cellule_cookbook_support::{NodeConfig, shutdown_signal}; +use cellule_runtime::{ApplicationId, BuildDescriptor, Digest}; +use cellule_store::Store; +use object_store::{memory::InMemory, path::Path}; +use utoipa::OpenApi as _; + +use application::{ORDERS, OrdersApp, ReadTotal, SetTotal}; +use auth::{ALPHA, BETA, ReadOrders, WriteOrders}; +use journal::Journal; +use proposal::ServiceNode; + +type AppResult = Result>; + +pub struct ServiceState { + node: ServiceNode, + journal: Journal, +} + +#[derive(utoipa::OpenApi)] +#[openapi(paths(auth::ready, recovery::resolve))] +struct ManualRoutes; + +async fn serve(state: Arc) -> AppResult<()> { + let (routes, mut document) = + CellApi::>::new(state.node.application())? + .command::( + ORDERS, + EndpointSpec::new("/orders/total", "setTotal"), + )? + .query::( + ORDERS, + EndpointSpec::new("/orders/total/read", "readTotal"), + )? + .into_router() + .split_for_parts(); + document.merge(ManualRoutes::openapi()); + document.info.title = "Orders service".into(); + document.info.version = env!("CARGO_PKG_VERSION").into(); + + // The same service that authenticates requests documents its security. + use utoipa::openapi::{ + response::ResponseBuilder, + security::{HttpAuthScheme, HttpBuilder, SecurityRequirement, SecurityScheme}, + }; + document + .components + .get_or_insert_with(Default::default) + .add_security_scheme( + "bearerAuth", + SecurityScheme::Http(HttpBuilder::new().scheme(HttpAuthScheme::Bearer).build()), + ); + for path in document.paths.paths.values_mut() { + if let Some(operation) = path.post.as_mut() { + operation.security = Some(vec![SecurityRequirement::new( + "bearerAuth", + Vec::::new(), + )]); + for (code, description) in [ + ("401", "Authentication failed"), + ("403", "Caller lacks write permission"), + ] { + operation.responses.responses.insert( + code.into(), + ResponseBuilder::new() + .description(description) + .build() + .into(), + ); + } + } + } + let router = Router::new() + .merge(routes) + .route( + "/orders/total/resolve/{request_id}", + post(recovery::resolve), + ) + .route("/ready", get(auth::ready)) + .route("/openapi.json", get(move || async move { Json(document) })) + .layer(DefaultBodyLimit::max(4096)) + .with_state(state); + + let bind = std::env::var("CELLULE_EXAMPLE_BIND").unwrap_or_else(|_| "127.0.0.1:3002".into()); + let listener = tokio::net::TcpListener::bind(bind).await?; + println!("Orders service: http://{}", listener.local_addr()?); + let (signal_tx, signal_rx) = tokio::sync::oneshot::channel(); + axum::serve(listener, router) + .with_graceful_shutdown(async move { + let signal = shutdown_signal().await; + println!("Stopping HTTP; waiting for accepted requests"); + let _ = signal_tx.send(signal); + }) + .await?; + signal_rx.await??; + Ok(()) +} + +#[tokio::main] +async fn main() -> AppResult<()> { + // Declare and compile once; the same artifact feeds node and OpenAPI. + let compiled = Arc::new(OrdersApp::compile(BuildDescriptor { + source_revision: format!("example-source:{:?}", application::source_digest()), + cargo_lock_digest: Digest::from_bytes( + *blake3::hash(include_bytes!("../Cargo.lock")).as_bytes(), + ), + })?); + + // This tutorial uses ephemeral authoritative storage and local credentials. + // A real service supplies its durable Store and persistent state directory. + let files = tempfile::tempdir()?; + let journal = Journal::open(files.path().join("commands.sqlite"))?; + let startup = ServiceNode::start( + compiled, + Store::new(Arc::new(InMemory::new())), + NodeConfig { + state_directory: files.path().join("node"), + storage_prefix: Path::from("builder-service"), + application_id: ApplicationId::from_bytes([7; 16]), + }, + &[ALPHA, BETA], + ) + .await; + let node = match startup { + Ok(node) => node, + Err(error) => { + let retained = files.keep(); + eprintln!( + "startup failed; retained local evidence at {}", + retained.display() + ); + return Err(error); + } + }; + let state = Arc::new(ServiceState { node, journal }); + + // This catches route construction, bind and serving errors as well as signals. + let serving = serve(state.clone()).await; + println!("Draining node with lease renewal still active"); + let shutdown = state.node.shutdown().await; + if let Err(error) = &shutdown { + eprintln!("node drain/withdrawal failed: {error}"); + let retained = files.keep(); + eprintln!("retained local evidence at {}", retained.display()); + } else { + println!("Node drained; enrollment withdrawn"); + } + serving?; + shutdown?; + Ok(()) +} diff --git a/examples/application-builder-service/src/proposal.rs b/examples/application-builder-service/src/proposal.rs new file mode 100644 index 00000000..6e9a6860 --- /dev/null +++ b/examples/application-builder-service/src/proposal.rs @@ -0,0 +1,169 @@ +//! Application-owned prototype of the proposed conveniences, using current APIs. + +use std::sync::Arc; + +use cellule_app::{ + ApplicationBuilder, ApplicationHandle, CellApplication, CellType, CompiledApplication, +}; +use cellule_cookbook_support::{LocalNode, NodeConfig}; +use cellule_runtime::{CellModule, Error, NamespaceId, TenantId}; +use cellule_store::Store; + +use crate::{ + AppResult, + application::{ORDERS, Orders, OrdersApp}, +}; + +pub struct CellBinding { + namespace: NamespaceId, + name: &'static str, + database_bytes: u64, + capture_bytes: u64, +} + +impl CellBinding { + // This prototype deliberately supports fixed-shard partition version one. + pub fn sharded(namespace: NamespaceId, name: &'static str) -> Self { + Self { + namespace, + name, + database_bytes: 64 << 20, + capture_bytes: 16 << 20, + } + } + + pub fn with_limits(mut self, database_bytes: u64, capture_bytes: u64) -> Self { + self.database_bytes = database_bytes; + self.capture_bytes = capture_bytes; + self + } +} + +pub trait ApplicationBuilderExt { + fn module( + &mut self, + module: M, + bindings: impl IntoIterator, + ) -> cellule_runtime::Result<()>; +} + +impl ApplicationBuilderExt for ApplicationBuilder { + fn module( + &mut self, + module: M, + bindings: impl IntoIterator, + ) -> cellule_runtime::Result<()> { + let descriptor = module.descriptor(); + let cells: Vec = bindings + .into_iter() + .map(|binding| { + let namespace = descriptor + .namespaces + .iter() + .find(|namespace| namespace.id == binding.namespace) + .ok_or(Error::Registry( + "Cell binding namespace is absent from module", + ))?; + CellType::new( + M::NAME, + binding.name, + namespace.id, + namespace.role, + namespace.shards, + )? + .with_schema_range(descriptor.schema_min, descriptor.schema_max)? + .with_limits(binding.database_bytes, binding.capture_bytes) + }) + .collect::>()?; + self.register(module)?; + for cell in cells { + self.cell_type(cell)?; + } + // ApplicationBuilder::finish remains the canonical full-contract check. + Ok(()) + } +} + +pub struct ServiceNode { + application: Arc, + node: LocalNode, +} + +impl ServiceNode { + pub async fn start( + application: Arc, + store: Store, + config: NodeConfig, + provisioned_tenants: &[TenantId], + ) -> AppResult { + // LocalNode owns the only CellNode/runtime, directory enrollment, + // renewable lease and supervised task group. + let node = LocalNode::start(application.clone(), store, config).await?; + let setup: AppResult<()> = async { + for tenant in provisioned_tenants { + let app = node.application_handle::(*tenant)?; + let target = app.target_for_scope(ORDERS, b"orders")?; + node.open_cell(&target, &Orders).await?; + } + Ok(()) + } + .await; + if let Err(error) = setup { + if let Err(cleanup) = node.shutdown().await { + eprintln!("startup cleanup failed: {cleanup}"); + } + return Err(error); + } + Ok(Self { application, node }) + } + + pub fn application(&self) -> &CompiledApplication { + &self.application + } + + pub fn scope( + &self, + authorized_tenant: TenantId, + ) -> cellule_cookbook_support::Result> { + self.node.application_handle(authorized_tenant) + } + + pub fn is_ready(&self) -> bool { + self.node.is_ready() + } + + pub async fn shutdown(&self) -> cellule_cookbook_support::Result<()> { + // LocalNode drains accepted work with lease renewal still live, then + // its lease-maintenance task withdraws the latest directory generation. + self.node.shutdown().await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cellule_runtime::{BuildDescriptor, CatalogRole, Digest}; + + #[test] + fn combined_registration_preserves_canonical_application_and_release_bytes() + -> cellule_runtime::Result<()> { + let build = BuildDescriptor { + source_revision: "builder-example-test".into(), + cargo_lock_digest: Digest::from_bytes([9; 32]), + }; + let proposed = OrdersApp::compile(build.clone())?; + let mut legacy = ApplicationBuilder::new(OrdersApp::NAME, build)?; + legacy.register(Orders)?; + legacy.cell_type( + CellType::new(Orders::NAME, "orders", ORDERS, CatalogRole::Sql, 1)? + .with_limits(64 << 20, 16 << 20)?, + )?; + let legacy = legacy.finish()?; + assert_eq!(proposed.descriptor_bytes(), legacy.descriptor_bytes()); + assert_eq!( + proposed.registry().release_bytes(), + legacy.registry().release_bytes() + ); + Ok(()) + } +} diff --git a/examples/application-builder-service/src/recovery.rs b/examples/application-builder-service/src/recovery.rs new file mode 100644 index 00000000..f40c1e66 --- /dev/null +++ b/examples/application-builder-service/src/recovery.rs @@ -0,0 +1,91 @@ +use axum::{ + Json, + extract::Path, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use cellule_axum::{CellEndpoint, CellJson, HttpError, utoipa}; +use cellule_runtime::{ + Receipt, Resolution, + cell::executor::StoredOutcome, + client::{Committed, InvocationError}, + codec::{BoundedDecoder, WireValue}, +}; +use uuid::Uuid; + +use crate::{application::SetTotal, auth::WriteOrders}; + +#[utoipa::path( + post, + path = "/orders/total/resolve/{request_id}", + operation_id = "resolveTotal", + params(("request_id" = Uuid, Path, description = "Original command request ID")), + responses( + (status = 200, body = CellJson, description = "Committed original outcome or safely replayed command"), + (status = 404, description = "No retained evidence in the authorized Cell"), + (status = 409, description = "Original command expired or conflicts"), + (status = 503, description = "Outcome remains unknown") + ), + security(("bearerAuth" = [])) +)] +pub async fn resolve( + context: WriteOrders, + Path(request): Path, +) -> Result { + let Some((snapshot, input)) = context + .journal() + .load(*context.target().cell_id().as_bytes(), request) + .await? + else { + return Ok(StatusCode::NOT_FOUND.into_response()); + }; + let restored = context + .application() + .restore_command::(snapshot, input)?; + let mut response = match context.application().resolve(restored.evidence()).await? { + Resolution::Committed(outcome) => { + let receipt = Receipt { + cell: restored.evidence().target().cell_id(), + incarnation: restored.evidence().incarnation(), + commit_sequence: outcome.commit_sequence(), + }; + let reply = CellJson { + output: outcome.result(), + receipt, + } + .try_map(|bytes| { + let mut decoder = BoundedDecoder::new(bytes, 1024)?; + let output = i64::decode(&mut decoder)?; + decoder.finish()?; + Ok::<_, cellule_runtime::codec::CodecError>(output) + })?; + match outcome { + StoredOutcome::Success { .. } => reply.into_response(), + StoredOutcome::Rejected { .. } => { + HttpError::from(InvocationError::Rejected(Box::new(Committed { + output: reply.output, + receipt, + }))) + .into_response() + } + } + } + // Only authoritative absence permits replay of these original bytes. + Resolution::Absent => CellJson::from(restored.execute().await?).into_response(), + Resolution::Unknown => ( + StatusCode::SERVICE_UNAVAILABLE, + Json(serde_json::json!({"state": "unknown"})), + ) + .into_response(), + Resolution::Expired => ( + StatusCode::CONFLICT, + Json(serde_json::json!({"state": "expired"})), + ) + .into_response(), + }; + response.headers_mut().insert( + "cache-control", + axum::http::HeaderValue::from_static("no-store"), + ); + Ok(response) +} From 477a8d8225a811e449573679c325379ef0fb8248 Mon Sep 17 00:00:00 2001 From: forhappy Date: Sun, 4 Oct 2026 15:29:17 -0700 Subject: [PATCH 2/7] feat(app): standardize module registration and scoped bindings --- .github/workflows/rust.yml | 8 +- cookbook/support/src/node.rs | 18 +- crates/cellule-app/README.md | 63 +++- crates/cellule-app/docs/examples.md | 6 + crates/cellule-app/docs/invocation.md | 10 +- crates/cellule-app/docs/topology.md | 26 +- crates/cellule-app/src/binding.rs | 106 +++++++ crates/cellule-app/src/cell_binding.rs | 109 +++++++ crates/cellule-app/src/lib.rs | 67 +++- crates/cellule-app/tests/bindings.rs | 298 ++++++++++++++++++ crates/cellule-app/tests/integration.rs | 1 + crates/cellule-host/README.md | 25 +- crates/cellule-host/src/lib.rs | 2 +- crates/cellule-host/src/node/application.rs | 35 ++ crates/cellule-host/src/node/mod.rs | 3 +- crates/cellule-host/tests/node.rs | 1 + crates/cellule-host/tests/node/application.rs | 64 ++++ docs/api.md | 95 +++--- .../application-builder-service/README.md | 19 +- .../src/application.rs | 4 +- .../application-builder-service/src/auth.rs | 5 +- .../application-builder-service/src/main.rs | 4 +- .../src/proposal.rs | 169 ---------- .../src/service.rs | 63 ++++ 24 files changed, 922 insertions(+), 279 deletions(-) create mode 100644 crates/cellule-app/src/binding.rs create mode 100644 crates/cellule-app/src/cell_binding.rs create mode 100644 crates/cellule-app/tests/bindings.rs create mode 100644 crates/cellule-host/src/node/application.rs create mode 100644 crates/cellule-host/tests/node/application.rs delete mode 100644 examples/application-builder-service/src/proposal.rs create mode 100644 examples/application-builder-service/src/service.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0ef4e135..2b6cad65 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -50,6 +50,10 @@ jobs: run: python3 scripts/release.py check - name: Check run: cargo check --workspace --all-targets --all-features --locked + - name: Verify application builder embedding example + run: | + cargo fmt --manifest-path examples/application-builder-service/Cargo.toml --check + cargo clippy --manifest-path examples/application-builder-service/Cargo.toml --all-targets --locked -- -D warnings - name: Documentation links run: RUSTDOCFLAGS='-D warnings' cargo doc --workspace --all-features --no-deps --locked - name: Test @@ -136,4 +140,6 @@ jobs: with: toolchain: 1.97.0 - name: Check Rust 1.97 minimum - run: cargo check --workspace --all-targets --all-features --locked + run: | + cargo check --workspace --all-targets --all-features --locked + cargo check --manifest-path examples/application-builder-service/Cargo.toml --all-targets --locked diff --git a/cookbook/support/src/node.rs b/cookbook/support/src/node.rs index 82b5268c..892dbbc6 100644 --- a/cookbook/support/src/node.rs +++ b/cookbook/support/src/node.rs @@ -1,6 +1,6 @@ use std::{path::PathBuf, sync::Arc, time::Duration}; -use cellule_app::{ApplicationHandle, CellApplication, CompiledApplication}; +use cellule_app::{ApplicationBinding, ApplicationHandle, CellApplication, CompiledApplication}; use cellule_host::{CellNode, CellNodeBuilder, CellNodeTaskGroup}; use cellule_ltx::{CellReplica, DiskBudget, Host, Limits}; use cellule_runtime::{ @@ -297,15 +297,13 @@ impl LocalNode { &self, tenant: TenantId, ) -> Result> { - Ok(self.node.application_handle( - CellClient::local_runtime( - self.node.application().registry(), - self.node.runtime(), - self.layout.clone(), - ), - tenant, - self.application_id, - )?) + Ok(self.application_binding::()?.scope(tenant)) + } + + /// Binds a reusable typed application factory to this node's existing resources. + /// Authorize each tenant before creating a scoped capability from the binding. + pub fn application_binding(&self) -> Result> { + Ok(self.node.bind_local_application(self.layout.clone())?) } /// Provisions or restores a declared Cell, serializing concurrent local acquisition. diff --git a/crates/cellule-app/README.md b/crates/cellule-app/README.md index 775fcb61..53d3584e 100644 --- a/crates/cellule-app/README.md +++ b/crates/cellule-app/README.md @@ -1,7 +1,8 @@ # cellule-app Declare a stable Cell topology, compile native Rust modules, and expose typed -application handles. The host owns runtime lifecycle and network wiring. +application handles. `cellule-host` manages runtime lifecycle; the embedding +service owns network wiring and authorization. Each SQL Cell owns its SQLite database, request ledger, and capture/recovery lineage. Cells can share worker threads and resource budgets within a host, @@ -9,15 +10,61 @@ and run on different nodes under separate fenced ownership. The application builder declares Cell types, partitions, schemas, and operations; the host handles placement, activation, routing, and recovery. -```mermaid -flowchart LR - Modules[Native modules] --> Builder[ApplicationBuilder] - Topology[Cell types] --> Builder - Builder --> Descriptor[CompiledApplication] - Descriptor --> Host[CellNode] - Descriptor --> Handle[ApplicationHandle] +```text +Module descriptor + explicit CellBinding + │ + ▼ + ApplicationBuilder::module + │ + ▼ + CompiledApplication + │ + ▼ + ApplicationBinding + existing client + │ + authorize tenant → scope(tenant) + │ + ▼ + ApplicationHandle → typed operations ``` +Register each module and all its namespace bindings together. The module supplies +role, shards, and schema range; you choose stable Cell names, namespace IDs, +partition modes, and limits. The compiler validates the choices before calling +the module's registration hook, then uses the existing canonical descriptor path. + +This helper accepts an already declared single-namespace module: + +```rust,no_run +use cellule_app::{ApplicationBuilder, CellBinding}; +use cellule_runtime::{CellModule, NamespaceId}; + +fn register_orders( + builder: &mut ApplicationBuilder, + module: M, + namespace: NamespaceId, +) -> cellule_runtime::Result<()> { + builder.module( + module, + [CellBinding::entity(namespace, "orders").with_limits(64 << 20, 16 << 20)], + ) +} +``` + +Use `CellBinding::sharded` for fixed shards, `entity` for hashed entity keys, +or `entity_uuid` for canonical UUID SQL Cells. Their descriptor bytes match the +equivalent explicit `CellType` declarations. Include every namespace in the +module exactly once. Module registration errors abort compilation; typed +registration hooks are not rolled back. + +Bind your configured `CellClient`, compiled artifact, and installation ID once +with `ApplicationBinding::::new`. Authorize each tenant before calling +`binding.scope(tenant)`. `cellule-host` provides +`CellNode::bind_local_application` to create this factory from its existing +runtime and a supplied storage layout. The same artifact can feed Axum/OpenAPI; +the [complete service example](../../examples/application-builder-service/README.md) +uses these framework building blocks. + | Guide | Topic | | --- | --- | | [API guide](../../docs/api.md) | Compile an application, bind handles, invoke commands, and handle outcomes. | diff --git a/crates/cellule-app/docs/examples.md b/crates/cellule-app/docs/examples.md index 66ef0fa8..267b7e20 100644 --- a/crates/cellule-app/docs/examples.md +++ b/crates/cellule-app/docs/examples.md @@ -18,6 +18,12 @@ modules + Cell types -> compiled descriptor -> catalog + fenced owner | `cargo run -p cellule-app --example workflow --locked` | Workflow, Activities | Start a durable run, have an `ActivitySupervisor` validate and execute its activity, then read the completed state. | | `cargo run -p cellule-app --example schedules --locked` | Cron, Effects | Register a fixed-interval schedule, drive one due maintenance tick, deliver its effect through a signed local peer loopback, and count the destination row. | +For a complete HTTP service, run +`cargo run --manifest-path examples/application-builder-service/Cargo.toml --locked`. +The [service guide](../../../examples/application-builder-service/README.md) +uses native module registration and scoped factories, authorized Axum routes, +OpenAPI, retained command evidence, and ordered host shutdown. + ## How each path works **[basic.rs](../examples/basic.rs)** compiles two modules with distinct diff --git a/crates/cellule-app/docs/invocation.md b/crates/cellule-app/docs/invocation.md index c47540d6..3854f68b 100644 --- a/crates/cellule-app/docs/invocation.md +++ b/crates/cellule-app/docs/invocation.md @@ -14,7 +14,9 @@ sequenceDiagram | Operation | Route and evidence | | --- | --- | -| `ApplicationHandle::new` | Checks application identity and registry digest before calls. | +| `ApplicationBinding::new` | Validates a configured client against the author type and compiled registry once during composition. | +| `ApplicationBinding::scope` | Creates a tenant capability from the bound client after application authorization; starts no runtime and opens no Cell. | +| `ApplicationHandle::new` | Checks author type and registry digest before calls. | | `cell_client!` | Binds declared namespace, operation IDs, and `CellKey` type. | | Command | Always uses current owner; durable outcome includes a receipt. | | Default query | `ReadPolicy::CurrentOwner` preserves owner ordering. | @@ -26,3 +28,9 @@ the client does not silently fall back to the owner. The host installs read replicas and the authenticated peer client. Application code sees only typed capabilities. Run the [SQL example](../examples/sql.rs) for a command and visible read-back. + +Keep one `ApplicationBinding` in service state and call `scope` after verifying +the caller's tenant permission. Each handle retains the bound installation ID, +compiled artifact, read policy, and Blob artifact store. Scoped invocations still +reject targets from another tenant, installation, or module. The factory itself +grants capabilities and must remain in trusted application code. diff --git a/crates/cellule-app/docs/topology.md b/crates/cellule-app/docs/topology.md index 03e69372..2531c58e 100644 --- a/crates/cellule-app/docs/topology.md +++ b/crates/cellule-app/docs/topology.md @@ -1,16 +1,24 @@ # Topology and descriptors -```mermaid -flowchart TD - Namespace[Stable namespace ID] --> CellType[CellType] - Role[Catalog role] --> CellType - Partition[Fixed shard or entity key] --> CellType - CellType --> Descriptor[Compiled descriptor digest] - Descriptor --> Routing[Typed client routing] +```text +Module descriptor CellBinding +role + shards + schema range stable namespace + name + partition + limits + │ │ + └──────────┬─────────────┘ + ▼ + ApplicationBuilder::module + │ + ▼ + canonical CellType descriptor + │ + ▼ + typed client routing ``` | Surface | Contract | | --- | --- | +| `CellBinding` | Explicit namespace, Cell name, partition mode and optional limits; role, shards and schema range come from the module. | +| `ApplicationBuilder::module` | Registers a module and all namespace bindings together; validates topology before invoking registration hooks. | | `CellType::new` | Declares module, name, namespace, role, and shard count. | | Fixed shards | Scope hashes to one declared shard. | | `with_entity_partitions` | One declared shard; canonical entity key derives a 33-byte partition. | @@ -21,6 +29,10 @@ flowchart TD Changing a stable namespace, role, partition scheme, or descriptor changes routing and persisted identity. Treat it as a versioned application change. +`CellBinding::sharded`, `entity`, and `entity_uuid` preserve the existing +partition versions and canonical descriptor encoding. The final compiler still +checks the module registry against every topology declaration. Keep the explicit +`register`/`cell_type` path when composing declarations independently. Fixed shards use partition version 1, hashed entity keys use version 2, and direct UUID partitions use version 3. These schemes have distinct descriptor bytes. UUID keys must be 16 bytes with a recognized version (1 through 8) diff --git a/crates/cellule-app/src/binding.rs b/crates/cellule-app/src/binding.rs new file mode 100644 index 00000000..d41792a7 --- /dev/null +++ b/crates/cellule-app/src/binding.rs @@ -0,0 +1,106 @@ +//! Validated application composition shared by authorized tenant scopes. + +use crate::{ + ApplicationHandle, ApplicationId, BlobArtifactStore, CellApplication, CellClient, + CompiledApplication, Error, PhantomData, Result, TenantId, +}; +use std::sync::Arc; + +/// A validated application/client binding that creates tenant-scoped capabilities. +/// +/// Construct once during trusted service startup and share it with authorization +/// code. [`Self::scope`] grants access to the supplied tenant: callers must +/// authenticate and authorize that tenant before calling it. This factory does +/// not open Cells, start a runtime, or perform authorization. +pub struct ApplicationBinding { + client: CellClient, + compiled: Arc, + application: ApplicationId, + marker: PhantomData A>, +} + +impl Clone for ApplicationBinding { + fn clone(&self) -> Self { + Self { + client: self.client.clone(), + compiled: Arc::clone(&self.compiled), + application: self.application, + marker: PhantomData, + } + } +} + +impl ApplicationBinding { + /// Validates the author type and client registry against the compiled artifact. + /// + /// The embedding application constructs and configures the client transport. + /// `cellule-host` can bind a local client to its existing runtime and layout. + pub fn new( + client: CellClient, + compiled: Arc, + application: ApplicationId, + ) -> Result { + if compiled.name() != A::NAME { + return Err(Error::Registry( + "application type differs from compiled application", + )); + } + if client.registry_digest() != compiled.registry().release_digest() { + return Err(Error::Registry( + "client registry differs from compiled application", + )); + } + Ok(Self { + client, + compiled, + application, + marker: PhantomData, + }) + } + + /// Creates a capability for an application-authorized tenant using the bound client. + /// + /// This clones the validated resources without reconstructing a client or + /// rereading registry descriptors. It does not validate a caller's permissions. + #[must_use] + pub fn scope(&self, authorized_tenant: TenantId) -> ApplicationHandle { + ApplicationHandle { + client: self.client.clone(), + compiled: Arc::clone(&self.compiled), + tenant: authorized_tenant, + application: self.application, + marker: PhantomData, + } + } + + /// Returns the immutable artifact used by every scoped capability. + #[must_use] + pub fn compiled(&self) -> &CompiledApplication { + &self.compiled + } + + /// Returns the stable application installation identity bound to the client. + #[must_use] + pub const fn application_id(&self) -> ApplicationId { + self.application + } + + /// Returns a binding with the explicit typed-query policy applied to all scopes. + /// + /// Commands and outcome resolution retain owner order. Replica queries + /// require configured readers and never silently fall back to owner reads. + #[must_use] + pub fn with_read_policy(&self, policy: cellule_runtime::client::ReadPolicy) -> Self { + let mut binding = self.clone(); + binding.client = binding.client.with_read_policy(policy); + binding + } + + /// Returns a binding whose scoped Blob capabilities share the supplied artifact store. + #[must_use] + pub fn with_blob_artifact_store(&self, store: BlobArtifactStore) -> Self { + let mut binding = self.clone(); + binding.client = binding.client.with_blob_artifact_store(store); + binding + } +} diff --git a/crates/cellule-app/src/cell_binding.rs b/crates/cellule-app/src/cell_binding.rs new file mode 100644 index 00000000..55610588 --- /dev/null +++ b/crates/cellule-app/src/cell_binding.rs @@ -0,0 +1,109 @@ +//! Explicit topology choices resolved against a module's existing descriptor. + +use crate::{CellType, Error, NamespaceId, Result}; +use cellule_runtime::{CatalogRole, ModuleDescriptor}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Partition { + Sharded, + Entity, + Uuid, +} + +/// One application-owned topology choice for a compiled module namespace. +/// +/// Used with [`crate::ApplicationBuilder::module`]. The descriptor supplies the +/// namespace role, shard count and module schema range. Stable names, identities +/// and partition schemes are explicit; no persisted routing choice is inferred. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CellBinding { + namespace: NamespaceId, + name: &'static str, + partition: Partition, + limits: Option<(u64, u64)>, +} + +impl CellBinding { + /// Selects existing fixed-shard partition version one. + #[must_use] + pub const fn sharded(namespace: NamespaceId, name: &'static str) -> Self { + Self { + namespace, + name, + partition: Partition::Sharded, + limits: None, + } + } + + /// Selects hashed entity partition version two in a single-shard namespace. + #[must_use] + pub const fn entity(namespace: NamespaceId, name: &'static str) -> Self { + Self { + namespace, + name, + partition: Partition::Entity, + limits: None, + } + } + + /// Selects canonical UUID partition version three in a single-shard SQL namespace. + #[must_use] + pub const fn entity_uuid(namespace: NamespaceId, name: &'static str) -> Self { + Self { + namespace, + name, + partition: Partition::Uuid, + limits: None, + } + } + + /// Sets explicit per-Cell database and capture ceilings. + /// + /// Without this choice, [`CellType::new`] supplies its existing defaults. + /// [`crate::ApplicationBuilder::module`] validates these values and all other + /// topology choices before invoking the module registration hook. + #[must_use] + pub const fn with_limits(mut self, database_bytes: u64, capture_bytes: u64) -> Self { + self.limits = Some((database_bytes, capture_bytes)); + self + } + + pub(crate) fn resolve(self, module: &ModuleDescriptor) -> Result { + let namespace = module + .namespaces + .iter() + .find(|namespace| namespace.id == self.namespace) + .ok_or(Error::Registry( + "Cell binding namespace is absent from module", + ))?; + let mut cell = match self.partition { + Partition::Uuid => { + if namespace.role != CatalogRole::Sql || namespace.shards != 1 { + return Err(Error::Registry( + "UUID Cell binding requires single-shard SQL", + )); + } + CellType::entity_uuid(module.name, self.name, namespace.id)? + } + Partition::Sharded | Partition::Entity => { + let cell = CellType::new( + module.name, + self.name, + namespace.id, + namespace.role, + namespace.shards, + )?; + if self.partition == Partition::Entity { + cell.with_entity_partitions()? + } else { + cell + } + } + } + .with_schema_range(module.schema_min, module.schema_max)?; + if let Some((database, capture)) = self.limits { + cell = cell.with_limits(database, capture)?; + } + Ok(cell) + } +} diff --git a/crates/cellule-app/src/lib.rs b/crates/cellule-app/src/lib.rs index 38d57e23..83e63940 100644 --- a/crates/cellule-app/src/lib.rs +++ b/crates/cellule-app/src/lib.rs @@ -52,6 +52,11 @@ const ENTITY_PARTITION_VERSION: u32 = 2; const UUID_PARTITION_VERSION: u32 = 3; const ENTITY_PARTITION_PREFIX: u8 = 1; +mod binding; +mod cell_binding; +pub use binding::ApplicationBinding; +pub use cell_binding::CellBinding; + /// One application-owned Cell topology declaration. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CellType { @@ -281,6 +286,50 @@ impl ApplicationBuilder { self.registry.register(module) } + /// Registers a module and one explicit Cell binding for each of its namespaces. + /// + /// Role, shards and schema range come from the module descriptor. Cell names, + /// namespace identities, partition modes and limits remain application-owned. + /// Topology errors are rejected before calling the module's registration hook. + /// [`Self::finish`] still verifies the complete registry and canonical descriptor. + /// As with [`Self::register`], a module registration failure aborts compilation; + /// its typed bindings are not rolled back. + pub fn module( + &mut self, + module: M, + bindings: impl IntoIterator, + ) -> Result<()> { + let descriptor = module.descriptor(); + let mut cells = Vec::new(); + for binding in bindings { + if self.cell_types.len() + cells.len() >= MAX_CELL_TYPES { + return Err(Error::Registry("Cell type count must be in 1..=128")); + } + let cell = binding.resolve(descriptor)?; + if self + .cell_types + .iter() + .chain(&cells) + .any(|existing: &CellType| { + existing.namespace == cell.namespace || existing.name == cell.name + }) + { + return Err(Error::Registry("duplicate Cell type identity")); + } + cells.push(cell); + } + if cells.len() != descriptor.namespaces.len() { + return Err(Error::Registry( + "module bindings must declare every namespace exactly once", + )); + } + self.register(module)?; + for cell in cells { + self.cell_type(cell)?; + } + Ok(()) + } + /// Adds one topology declaration and rejects duplicate stable identities. pub fn cell_type(&mut self, cell_type: CellType) -> Result<()> { cell_type.validate()?; @@ -456,23 +505,7 @@ impl ApplicationHandle { tenant: TenantId, application: ApplicationId, ) -> Result { - if compiled.name() != A::NAME { - return Err(Error::Registry( - "application type differs from compiled application", - )); - } - if client.registry_digest() != compiled.registry.release_digest() { - return Err(Error::Registry( - "client registry differs from compiled application", - )); - } - Ok(Self { - client, - tenant, - application, - compiled, - marker: PhantomData, - }) + Ok(ApplicationBinding::::new(client, compiled, application)?.scope(tenant)) } /// Returns an application capability with an explicit typed-query read policy. diff --git a/crates/cellule-app/tests/bindings.rs b/crates/cellule-app/tests/bindings.rs new file mode 100644 index 00000000..ec50aabd --- /dev/null +++ b/crates/cellule-app/tests/bindings.rs @@ -0,0 +1,298 @@ +//! Public composition contracts: persisted topology and tenant-scoped factories. + +use super::*; +use cellule_app::{ApplicationBinding, CellBinding, CompiledApplication}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +const PRIMARY: NamespaceId = NamespaceId::from_bytes([81; 16]); +const SECONDARY: NamespaceId = NamespaceId::from_bytes([82; 16]); + +struct Module { + descriptor: &'static ModuleDescriptor, + registrations: Arc, +} + +impl CellModule for Module { + const NAME: &'static str = "binding-module"; + fn descriptor(&self) -> &'static ModuleDescriptor { + self.descriptor + } + fn register(self, _: &mut RegistryBuilder) -> Result<()> { + self.registrations.fetch_add(1, Ordering::SeqCst); + Ok(()) + } +} + +fn descriptor(role: CatalogRole, count: usize) -> &'static ModuleDescriptor { + let namespaces = (0..count) + .map(|index| NamespaceDescriptor { + id: NamespaceId::from_bytes([81 + index as u8; 16]), + name: Box::leak(format!("namespace-{index}").into_boxed_str()), + role, + shards: if index == 1 { 4 } else { 1 }, + effect_targets: &[], + dead_letter: None, + }) + .collect::>(); + let migrations = [ + "-- binding schema one", + "-- binding schema two", + "-- binding schema three", + ] + .into_iter() + .enumerate() + .map(|(index, sql)| cellule_runtime::MigrationDescriptor { + version: index as u32 + 1, + sql, + digest: Digest::from_bytes(*blake3::hash(sql.as_bytes()).as_bytes()), + }) + .collect::>(); + Box::leak(Box::new(ModuleDescriptor { + name: Module::NAME, + source_digest: Digest::from_bytes([83; 32]), + retained_codes: &[], + schema_min: 1, + schema_max: 3, + migrations: Box::leak(migrations.into_boxed_slice()), + commands: &[], + queries: &[], + workflow_definitions: &[], + activity_types: &[], + namespaces: Box::leak(namespaces.into_boxed_slice()), + })) +} + +fn build(revision: &str) -> BuildDescriptor { + BuildDescriptor { + source_revision: revision.into(), + cargo_lock_digest: Digest::from_bytes([84; 32]), + } +} + +fn module(descriptor: &'static ModuleDescriptor) -> Module { + Module { + descriptor, + registrations: Arc::new(AtomicUsize::new(0)), + } +} + +struct App; +impl CellApplication for App { + const NAME: &'static str = "binding-app"; + fn register(builder: &mut ApplicationBuilder) -> Result<()> { + builder.module( + module(descriptor(CatalogRole::Sql, 2)), + [ + CellBinding::sharded(PRIMARY, "orders"), + CellBinding::sharded(SECONDARY, "inventory"), + ], + ) + } +} + +struct OtherApp; +impl CellApplication for OtherApp { + const NAME: &'static str = "other-app"; + fn register(_: &mut ApplicationBuilder) -> Result<()> { + Ok(()) + } +} + +struct QueryNeverDispatched; +impl cellule_runtime::Query for QueryNeverDispatched { + const MODULE: &'static str = Module::NAME; + const ID: u32 = 1; + const CODEC_VERSION: u32 = 1; + type Input = (); + type Output = (); + fn execute(_: &mut cellule_runtime::registry::QueryContext<'_>, (): ()) -> Result<()> { + panic!("scope rejection must happen before dispatch") + } +} + +#[test] +fn module_bindings_preserve_all_partition_descriptor_versions_and_schema_ranges() { + let descriptor = descriptor(CatalogRole::Sql, 2); + for mode in 1..=3 { + let primary = match mode { + 1 => CellBinding::sharded(PRIMARY, "orders"), + 2 => CellBinding::entity(PRIMARY, "orders"), + _ => CellBinding::entity_uuid(PRIMARY, "orders"), + } + .with_limits(8 << 20, 2 << 20); + let mut combined = ApplicationBuilder::new(App::NAME, build("same-release")).unwrap(); + combined + .module( + module(descriptor), + [primary, CellBinding::sharded(SECONDARY, "inventory")], + ) + .unwrap(); + let combined = combined.finish().unwrap(); + + let mut explicit = ApplicationBuilder::new(App::NAME, build("same-release")).unwrap(); + explicit.register(module(descriptor)).unwrap(); + let primary = match mode { + 1 => CellType::new(Module::NAME, "orders", PRIMARY, CatalogRole::Sql, 1).unwrap(), + 2 => CellType::new(Module::NAME, "orders", PRIMARY, CatalogRole::Sql, 1) + .unwrap() + .with_entity_partitions() + .unwrap(), + _ => CellType::entity_uuid(Module::NAME, "orders", PRIMARY).unwrap(), + } + .with_schema_range(1, 3) + .unwrap() + .with_limits(8 << 20, 2 << 20) + .unwrap(); + explicit.cell_type(primary).unwrap(); + explicit + .cell_type( + CellType::new(Module::NAME, "inventory", SECONDARY, CatalogRole::Sql, 4) + .unwrap() + .with_schema_range(1, 3) + .unwrap(), + ) + .unwrap(); + let explicit = explicit.finish().unwrap(); + assert_eq!(combined.cell_types(), explicit.cell_types()); + assert_eq!(combined.descriptor_bytes(), explicit.descriptor_bytes()); + assert_eq!( + combined.registry().release_bytes(), + explicit.registry().release_bytes() + ); + } +} + +#[test] +fn module_topology_errors_are_rejected_before_registration() { + let descriptor = descriptor(CatalogRole::Sql, 2); + let first = CellBinding::sharded(PRIMARY, "orders"); + let second = CellBinding::sharded(SECONDARY, "inventory"); + for bindings in [ + vec![first], + vec![first, first], + vec![first, CellBinding::sharded(SECONDARY, "orders")], + vec![ + first, + CellBinding::sharded(NamespaceId::from_bytes([99; 16]), "unknown"), + ], + vec![first.with_limits(0, 128), second], + vec![first, CellBinding::entity(SECONDARY, "inventory")], + vec![first, CellBinding::entity_uuid(SECONDARY, "inventory")], + ] { + let candidate = module(descriptor); + let registrations = candidate.registrations.clone(); + let mut builder = ApplicationBuilder::new(App::NAME, build("invalid")).unwrap(); + assert!(builder.module(candidate, bindings).is_err()); + assert_eq!(registrations.load(Ordering::SeqCst), 0); + // Invalid topology did not consume identities or partially register a module. + builder.module(module(descriptor), [first, second]).unwrap(); + assert!(builder.finish().is_ok()); + } +} + +#[test] +fn module_bindings_reject_conflicts_with_existing_topology_before_registration() { + let descriptor = descriptor(CatalogRole::Sql, 2); + for (namespace, name, shards) in [(PRIMARY, "reserved", 1), (SECONDARY, "orders", 4)] { + let mut builder = ApplicationBuilder::new(App::NAME, build("mixed-api")).unwrap(); + builder + .cell_type( + CellType::new(Module::NAME, name, namespace, CatalogRole::Sql, shards).unwrap(), + ) + .unwrap(); + let candidate = module(descriptor); + let registrations = candidate.registrations.clone(); + assert!(matches!( + builder.module( + candidate, + [ + CellBinding::sharded(PRIMARY, "orders"), + CellBinding::sharded(SECONDARY, "inventory"), + ], + ), + Err(Error::Registry("duplicate Cell type identity")) + )); + assert_eq!(registrations.load(Ordering::SeqCst), 0); + } +} + +#[test] +fn module_bindings_refuse_uuid_on_non_sql_and_excessive_cell_counts() { + for (descriptor, bindings) in [ + ( + descriptor(CatalogRole::Kv, 1), + vec![CellBinding::entity_uuid(PRIMARY, "orders")], + ), + ( + descriptor(CatalogRole::Sql, 129), + (0..129) + .map(|index| { + CellBinding::sharded( + NamespaceId::from_bytes([81 + index as u8; 16]), + Box::leak(format!("cell-{index}").into_boxed_str()), + ) + }) + .collect(), + ), + ] { + let module = module(descriptor); + let registrations = module.registrations.clone(); + let mut builder = ApplicationBuilder::new(App::NAME, build("invalid")).unwrap(); + assert!(builder.module(module, bindings).is_err()); + assert_eq!(registrations.load(Ordering::SeqCst), 0); + } +} + +#[tokio::test] +async fn application_binding_validates_once_and_preserves_tenant_and_application_scope() { + let compiled = Arc::new(App::compile(build("factory")).unwrap()); + let runtime = CellRuntime::new( + SqlWorkerPool::new(1, 4).unwrap(), + 16 << 20, + cellule_runtime::SessionId::from_bytes([85; 16]), + ) + .unwrap(); + let application = ApplicationId::from_bytes([86; 16]); + let layout = CellStorageLayout::new( + Store::new(Arc::new(InMemory::new())), + object_store::path::Path::from("bindings"), + *application.as_bytes(), + ); + let client = CellClient::local_runtime(compiled.registry(), runtime.clone(), layout); + assert!(matches!( + ApplicationBinding::::new(client.clone(), compiled.clone(), application), + Err(Error::Registry(_)) + )); + let other_release: CompiledApplication = App::compile(build("different-release")).unwrap(); + assert!(matches!( + ApplicationBinding::::new(client.clone(), Arc::new(other_release), application), + Err(Error::Registry(_)) + )); + let binding = ApplicationBinding::::new(client, compiled.clone(), application).unwrap(); + assert_eq!(binding.application_id(), application); + let alpha = binding.scope(TenantId::from_bytes([87; 16])); + let beta = binding.clone().scope(TenantId::from_bytes([88; 16])); + assert!(std::ptr::eq(alpha.compiled(), binding.compiled())); + let a = alpha.target_for_scope(PRIMARY, b"orders").unwrap(); + let b = beta.target_for_scope(PRIMARY, b"orders").unwrap(); + assert_eq!(a.application(), application); + assert_ne!(a.cell_id(), b.cell_id()); + assert!(matches!( + alpha.query::(&b, None, ()).await, + Err(InvocationError::NotStarted(Error::Identity(_))) + )); + let foreign = CellTarget::new( + a.tenant(), + ApplicationId::from_bytes([89; 16]), + PRIMARY, + a.partition(), + ) + .unwrap(); + assert!(matches!( + alpha + .query::(&foreign, None, ()) + .await, + Err(InvocationError::NotStarted(Error::Identity(_))) + )); + runtime.shutdown().await.unwrap(); +} diff --git a/crates/cellule-app/tests/integration.rs b/crates/cellule-app/tests/integration.rs index 97044ee8..408cc85d 100644 --- a/crates/cellule-app/tests/integration.rs +++ b/crates/cellule-app/tests/integration.rs @@ -66,6 +66,7 @@ use ed25519_dalek::SigningKey; use object_store::memory::InMemory; mod application; +mod bindings; mod commit; mod entities; mod fleet; diff --git a/crates/cellule-host/README.md b/crates/cellule-host/README.md index 46b63f1b..e01d5edf 100644 --- a/crates/cellule-host/README.md +++ b/crates/cellule-host/README.md @@ -3,13 +3,24 @@ `CellNode` owns one runtime, its admission ledger, facilities, and task group. An application supplies identity, provider, ingress, and authorization. -```mermaid -stateDiagram-v2 - [*] --> Starting - Starting --> Ready: lease and required components installed - Ready --> Draining: stop admission and producers - Draining --> Closing: accepted work drained - Closing --> Stopped: close log and withdraw session +Use `node.bind_local_application::(layout)` once during composition to get an +`ApplicationBinding` over the node's existing runtime. The supplied layout +determines the installation ID. Keep the factory in trusted service state and +call `binding.scope(tenant)` after authorizing the caller. It creates no runtime, +acquires no Cell, and leaves readiness and drain with the original node. + +`bind_application` accepts an already configured client, including an +application-owned remote transport. Both paths reject mismatched application +types and client registries. See the +[complete embedding service](../../examples/application-builder-service/README.md) +for scoped Axum routes and OpenAPI using these bindings. + +```text +Starting → Ready → Draining → Closing → Stopped + │ │ │ │ + lease and stop accepted close log; + components admission work withdraw + installed + producers drained session ``` | Guide | Topic | diff --git a/crates/cellule-host/src/lib.rs b/crates/cellule-host/src/lib.rs index 8e0abdb0..170c223d 100644 --- a/crates/cellule-host/src/lib.rs +++ b/crates/cellule-host/src/lib.rs @@ -54,7 +54,7 @@ pub mod read_replicas; mod status; mod tasks; -use cellule_app::{ApplicationHandle, CellApplication, CompiledApplication}; +use cellule_app::{ApplicationBinding, ApplicationHandle, CellApplication, CompiledApplication}; use cellule_runtime::Error; use cellule_runtime::cell::actor::{CellRuntime, CellRuntimeStats}; use cellule_runtime::cell::worker::SqlWorkerPool; diff --git a/crates/cellule-host/src/node/application.rs b/crates/cellule-host/src/node/application.rs new file mode 100644 index 00000000..51d4b0e5 --- /dev/null +++ b/crates/cellule-host/src/node/application.rs @@ -0,0 +1,35 @@ +//! Typed application factories over the node's canonical resources. + +use super::*; +use cellule_runtime::ltx::CellStorageLayout; + +impl CellNode { + /// Binds a product-configured client to this node's compiled application. + /// + /// Construct once and create handles with [`ApplicationBinding::scope`] after + /// authorizing each tenant. A different application type or registry is refused. + pub fn bind_application( + &self, + client: CellClient, + application: ApplicationId, + ) -> cellule_runtime::Result> { + ApplicationBinding::new(client, Arc::clone(&self.application), application) + } + + /// Binds a local application factory to the node's existing runtime and storage layout. + /// + /// The application supplies an already constructed authoritative layout. + /// Its application ID determines all scoped targets, avoiding a separate + /// identity argument. This does not start tasks, acquire Cells or open readiness. + /// The node remains the sole runtime owner and controls admission and drain. + pub fn bind_local_application( + &self, + layout: CellStorageLayout, + ) -> cellule_runtime::Result> { + let application = ApplicationId::from_bytes(*layout.application_id()); + self.bind_application( + CellClient::local_runtime(self.application.registry(), self.runtime.clone(), layout), + application, + ) + } +} diff --git a/crates/cellule-host/src/node/mod.rs b/crates/cellule-host/src/node/mod.rs index d7df7a19..fc192987 100644 --- a/crates/cellule-host/src/node/mod.rs +++ b/crates/cellule-host/src/node/mod.rs @@ -128,10 +128,11 @@ impl CellNode { tenant: TenantId, application: ApplicationId, ) -> cellule_runtime::Result> { - ApplicationHandle::new(client, Arc::clone(&self.application), tenant, application) + Ok(self.bind_application(client, application)?.scope(tenant)) } } +mod application; mod components; mod drain; mod fleet; diff --git a/crates/cellule-host/tests/node.rs b/crates/cellule-host/tests/node.rs index 28f81398..7968b29b 100644 --- a/crates/cellule-host/tests/node.rs +++ b/crates/cellule-host/tests/node.rs @@ -101,6 +101,7 @@ mod node { Arc::new(builder.finish().unwrap()) } + pub mod application; pub mod builder; pub mod components; pub mod durability; diff --git a/crates/cellule-host/tests/node/application.rs b/crates/cellule-host/tests/node/application.rs new file mode 100644 index 00000000..84ef1cd4 --- /dev/null +++ b/crates/cellule-host/tests/node/application.rs @@ -0,0 +1,64 @@ +//! Public application factories bound to one host runtime and storage identity. + +use super::*; +use cellule_app::{ApplicationBuilder, CellApplication}; +use cellule_runtime::{CellClient, NamespaceId, TenantId, ltx::CellStorageLayout}; +use cellule_store::Store; +use object_store::{memory::InMemory, path::Path}; + +struct App; +impl CellApplication for App { + const NAME: &'static str = "host-test"; + fn register(_: &mut ApplicationBuilder) -> cellule_runtime::Result<()> { + Ok(()) + } +} + +struct OtherApp; +impl CellApplication for OtherApp { + const NAME: &'static str = "other-host"; + fn register(_: &mut ApplicationBuilder) -> cellule_runtime::Result<()> { + Ok(()) + } +} + +#[tokio::test] +async fn local_factory_uses_the_host_artifact_and_layout_application_identity() { + let node = CellNodeBuilder::new(application()) + .with_runtime(SqlWorkerPool::new(1, 4).unwrap(), 16 << 20) + .with_replica_host(ReplicaHost::default()) + .with_session(SessionId::from_bytes([11; 16])) + .build_unleased_for_maintenance() + .unwrap(); + let application = ApplicationId::from_bytes([12; 16]); + let layout = CellStorageLayout::new( + Store::new(Arc::new(InMemory::new())), + Path::from("host-binding"), + *application.as_bytes(), + ); + let binding = node.bind_local_application::(layout.clone()).unwrap(); + assert!(std::ptr::eq(binding.compiled(), node.application())); + assert_eq!(binding.application_id(), application); + let target = binding + .scope(TenantId::from_bytes([13; 16])) + .target_for_scope(NamespaceId::from_bytes([2; 16]), b"orders") + .unwrap(); + assert_eq!(target.application(), application); + assert!(matches!( + node.bind_local_application::(layout.clone()), + Err(Error::Registry(_)) + )); + + let mut registry = RegistryBuilder::new(BuildDescriptor { + source_revision: "other-release".into(), + cargo_lock_digest: Digest::from_bytes([7; 32]), + }); + registry.register(Module).unwrap(); + let client = + CellClient::local_runtime(Arc::new(registry.finish().unwrap()), node.runtime(), layout); + assert!(matches!( + node.bind_application::(client, application), + Err(Error::Registry(_)) + )); + node.shutdown().await.unwrap(); +} diff --git a/docs/api.md b/docs/api.md index 7347aa9b..8649a5d1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -10,8 +10,8 @@ including catalog provisioning and a local runtime. | You need to… | Start with | Ownership | | --- | --- | --- | -| Declare modules and stable topology | [`CellApplication`, `ApplicationBuilder`, `CellType`](../crates/cellule-app/src/lib.rs) | Application author | -| Select a Cell and call typed operations | [`ApplicationHandle`, `cell_client!`](../crates/cellule-app/src/lib.rs) | Application author | +| Declare modules and stable topology | [`CellApplication`, `ApplicationBuilder::module`, `CellBinding`](../crates/cellule-app/README.md) | Application author | +| Bind once, then select a Cell and call typed operations | [`ApplicationBinding`, `ApplicationHandle`, `cell_client!`](../crates/cellule-app/README.md) | Application author | | Use SQL, KV, Blob, Queue, Cron, or Workflow | [Typed primitive capabilities](#primitive-capabilities) | Application author | | Implement a custom operation | [`CellModule`, `Command`, `Query`, `WireValue`](../crates/cellule-runtime/docs/rust-api.md) | Module author | | Start and drain a serving node | [`CellNodeBuilder`, `CellNode`](../crates/cellule-host/README.md) | Service | @@ -24,42 +24,44 @@ controls providers and network policy. ## 1. Compile a stable application A `CellModule` declares its schema migrations, namespace, operation IDs, codec -versions, and bounds. `CellApplication::register` adds each module and a -matching `CellType`. Compilation checks that every registry namespace has one +versions, and bounds. In `CellApplication::register`, use +`ApplicationBuilder::module(module, bindings)` to register the module and one +explicit `CellBinding` per namespace together. The descriptor supplies role, +shards, and schema range. Compilation checks that every registry namespace has one matching topology declaration and emits canonical descriptor bytes and a digest. It does not provision or start a Cell. -This complete topology function is also compiled as a doctest in the +This helper for an already declared single-namespace module is also compiled in the [`cellule-app` crate README](../crates/cellule-app/README.md): -```rust -use cellule_app::CellType; -use cellule_runtime::{CatalogRole, NamespaceId}; - -fn orders_topology() -> cellule_runtime::Result { - CellType::new( - "orders", - "orders", - NamespaceId::from_bytes([1; 16]), - CatalogRole::Sql, - 1, - )? - .with_entity_partitions() +```rust,no_run +use cellule_app::{ApplicationBuilder, CellBinding}; +use cellule_runtime::{CellModule, NamespaceId}; + +fn register_orders( + builder: &mut ApplicationBuilder, + module: M, + namespace: NamespaceId, +) -> cellule_runtime::Result<()> { + builder.module( + module, + [CellBinding::entity(namespace, "orders").with_limits(64 << 20, 16 << 20)], + ) } - -assert!(orders_topology().is_ok()); ``` -`CellType::new` takes the module name, Cell type name, stable namespace ID, -catalog role, and shard count. Choose one partition scheme: +Choose stable names and namespace IDs, limits, and one partition scheme: | Scheme | API | Target rule | | --- | --- | --- | -| Fixed shards | `CellType::new(..., shards)` | `partition_for_scope` hashes scope bytes to one declared shard. | -| Entity Cells | `.with_entity_partitions()` | `entity_partition` derives one 33-byte partition from a nonempty canonical key; the namespace declares one shard. | - -`with_schema_range` sets the accepted schema interval; `with_limits` sets the -per-Cell database and capture ceilings. Namespaces, roles, partition schemes, +| Fixed shards | `CellBinding::sharded(namespace, name)` | `partition_for_scope` hashes scope bytes to one declared shard. | +| Entity Cells | `CellBinding::entity(namespace, name)` | `entity_partition` derives one 33-byte partition from a nonempty canonical key; the namespace declares one shard. | +| UUID SQL Cells | `CellBinding::entity_uuid(namespace, name)` | The partition is the canonical 16-byte UUID; the SQL namespace declares one shard. | + +The lower-level `register` and `cell_type` methods accept explicit `CellType` +declarations and emit the same descriptor bytes. With that path, +`with_schema_range` must match the module's schema interval. `with_limits` sets +the per-Cell database and capture ceilings. Namespaces, roles, partition schemes, shard counts, operation IDs, and descriptor bytes are compatibility contracts. Changing them can change routing or persisted identity. Read the [topology guide](../crates/cellule-app/docs/topology.md) before changing a @@ -90,20 +92,33 @@ registration code. ## 2. Bind a client and select a Cell -Once a service has provisioned a catalog entry, established an owner, and -started a `CellClient`, bind that client to the compiled application and the -service-selected tenant and application IDs. These lines come from the -[runnable SQL example](../crates/cellule-app/examples/sql.rs): - -```rust -let client = CellClient::local(registry, handle); -let typed = ApplicationHandle::::new(client, application, tenant, application_id)?; -let sql = typed.sql::(target)?; +Bind the configured client to the compiled application and installation ID once +with `ApplicationBinding::::new`. A local serving node supplies the same +factory with `node.bind_local_application::(layout)`, deriving the installation +ID from the supplied storage layout and using the existing runtime. + +```rust,no_run +use cellule_app::{ApplicationBinding, CellApplication}; +use cellule_host::CellNode; +use cellule_runtime::ltx::CellStorageLayout; + +fn bind_service( + node: &CellNode, + layout: CellStorageLayout, +) -> cellule_runtime::Result> { + node.bind_local_application::(layout) +} ``` -`ApplicationHandle::new` rejects an author type or client registry that does -not match the compiled artifact. Its calls also reject targets outside the -bound tenant, application, namespace, or declared partition scheme. +Keep this factory in trusted service state. After authenticating and authorizing +the tenant, call `binding.scope(tenant)` to create its `ApplicationHandle`. +The factory validates the author type and client registry once; every handle +still rejects targets outside its tenant, application, namespace, or declared +partition scheme. `ApplicationHandle::new` remains available for directly binding +one tenant through the same validation path. Binding alone does not activate +Cells; the host must provision the catalog and establish an owner before calls. +The [embedding service](../examples/application-builder-service/README.md) uses +this flow with Axum/OpenAPI and ordered shutdown. Use `target_for_scope(namespace, scope)` to derive a target from the declared fixed-shard or entity scheme. The local SQL example selects its sole fixed @@ -187,7 +202,7 @@ SQL and source effects select an explicit target. | `activities::()` | `WorkflowActivities` | Capability consumed by `ActivitySupervisor` for external work | | `effects::(target)` | `EffectSource` | `claim`, `validate`, `status`, `ack`, `retry` on a source Cell | -Blob handles require `with_blob_artifact_store` on the application handle; +Blob handles require `with_blob_artifact_store` on the binding or application handle; the [Blob example](../crates/cellule-app/examples/blob.rs) shows the complete upload and receipt-bound read. Queue and effect lease validation use the current owner even when ordinary queries use a replica. Activities and diff --git a/examples/application-builder-service/README.md b/examples/application-builder-service/README.md index a7fd31a0..7a53889c 100644 --- a/examples/application-builder-service/README.md +++ b/examples/application-builder-service/README.md @@ -1,9 +1,11 @@ # Application builder → Axum service -A complete embedding application illustrating the proposed builder ergonomics. -Run it against the current Cellule checkout; no framework API implementation is -assumed. The conveniences are implemented locally in [proposal.rs](src/proposal.rs) -and delegate to the existing application compiler and cookbook host assembly. +A complete embedding application using native Cellule registration and scoped +handle factories. `CellBinding`, `ApplicationBuilder::module`, and +`ApplicationBinding` belong to [cellule-app](../../crates/cellule-app/README.md); +the local binding method belongs to [cellule-host](../../crates/cellule-host/README.md). +Application-owned startup and tenant provisioning remain in +[service.rs](src/service.rs), using the existing cookbook host assembly. ```text Orders module + Cell binding @@ -27,7 +29,7 @@ Shutdown: stop HTTP → finish requests → drain node → withdraw enrollment | File | Responsibility | | --- | --- | | [application.rs](src/application.rs) | Declare the SQL module, migration, command/query contracts, and application binding. | -| [proposal.rs](src/proposal.rs) | Prototype `builder.module`, explicit fixed-shard `CellBinding`, node startup, handle factory, and shutdown. | +| [service.rs](src/service.rs) | Application startup and provisioning policy; retain one native `ApplicationBinding` for all authorized requests. | | [auth.rs](src/auth.rs) | Authenticate a credential, authorize read/write access, then derive a scoped handle and target. | | [journal.rs](src/journal.rs) | Atomically retain the prepared snapshot and exact encoded input before dispatch. | | [recovery.rs](src/recovery.rs) | Resolve original evidence; replay only after authoritative absence. | @@ -37,7 +39,7 @@ Shutdown: stop HTTP → finish requests → drain node → withdraw enrollment descriptor. Namespace identity, Cell name, limits, and fixed-shard partition selection remain explicit. `ApplicationBuilder::finish` still performs the canonical whole-application validation. Treat any registration error as a -failed compilation; this helper does not roll back a partially mutated builder. +failed compilation; typed registration hooks are not rolled back. `ServiceNode` wraps the cookbook's `LocalNode`, which owns one `CellNode` and one runtime. The host owns directory enrollment, lease renewal, supervision, @@ -64,8 +66,9 @@ publication here is relative to that provider's lifetime. For persistent service state, supply a durable, successfully probed `Store`, stable application identity, persistent state/journal directories, and the service's actual identity and authorization implementation. The cookbook host is a single-node -recipe supporting schema version one; this prototype is not the general -production host startup API proposed for `cellule-host`. +recipe supporting schema version one. Managed host startup and general Cell +activation/recovery remain a later framework stage; the registration and +scoped-binding building blocks used here are framework APIs. ## Call the service diff --git a/examples/application-builder-service/src/application.rs b/examples/application-builder-service/src/application.rs index 94c449f1..ddb5ad64 100644 --- a/examples/application-builder-service/src/application.rs +++ b/examples/application-builder-service/src/application.rs @@ -1,6 +1,6 @@ use std::sync::OnceLock; -use cellule_app::{ApplicationBuilder, CellApplication}; +use cellule_app::{ApplicationBuilder, CellApplication, CellBinding}; use cellule_runtime::{ CatalogRole, CellModule, Command, Digest, MigrationDescriptor, ModuleDescriptor, NamespaceDescriptor, NamespaceId, Query, RegistryBuilder, @@ -8,8 +8,6 @@ use cellule_runtime::{ registry::{CommandContext, CommandResult, OperationDescriptor, QueryContext}, }; -use crate::proposal::{ApplicationBuilderExt, CellBinding}; - pub const ORDERS: NamespaceId = NamespaceId::from_bytes([23; 16]); const SCHEMA: &str = "CREATE TABLE totals (id INTEGER PRIMARY KEY, cents INTEGER NOT NULL)"; const COMMANDS: [OperationDescriptor; 1] = [operation(SetTotal::ID)]; diff --git a/examples/application-builder-service/src/auth.rs b/examples/application-builder-service/src/auth.rs index 20d2d8aa..554c426e 100644 --- a/examples/application-builder-service/src/auth.rs +++ b/examples/application-builder-service/src/auth.rs @@ -72,10 +72,7 @@ impl FromRequestParts> for Authorized(principal.tenant) - .map_err(|error| HttpError::internal(error).into_response())?; + let app = state.node.scope(principal.tenant); let target = app .target_for_scope(ORDERS, b"orders") .map_err(|error| HttpError::from(error).into_response())?; diff --git a/examples/application-builder-service/src/main.rs b/examples/application-builder-service/src/main.rs index 455d9162..2d8648c5 100644 --- a/examples/application-builder-service/src/main.rs +++ b/examples/application-builder-service/src/main.rs @@ -1,8 +1,8 @@ mod application; mod auth; mod journal; -mod proposal; mod recovery; +mod service; use std::sync::Arc; @@ -22,7 +22,7 @@ use utoipa::OpenApi as _; use application::{ORDERS, OrdersApp, ReadTotal, SetTotal}; use auth::{ALPHA, BETA, ReadOrders, WriteOrders}; use journal::Journal; -use proposal::ServiceNode; +use service::ServiceNode; type AppResult = Result>; diff --git a/examples/application-builder-service/src/proposal.rs b/examples/application-builder-service/src/proposal.rs deleted file mode 100644 index 6e9a6860..00000000 --- a/examples/application-builder-service/src/proposal.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! Application-owned prototype of the proposed conveniences, using current APIs. - -use std::sync::Arc; - -use cellule_app::{ - ApplicationBuilder, ApplicationHandle, CellApplication, CellType, CompiledApplication, -}; -use cellule_cookbook_support::{LocalNode, NodeConfig}; -use cellule_runtime::{CellModule, Error, NamespaceId, TenantId}; -use cellule_store::Store; - -use crate::{ - AppResult, - application::{ORDERS, Orders, OrdersApp}, -}; - -pub struct CellBinding { - namespace: NamespaceId, - name: &'static str, - database_bytes: u64, - capture_bytes: u64, -} - -impl CellBinding { - // This prototype deliberately supports fixed-shard partition version one. - pub fn sharded(namespace: NamespaceId, name: &'static str) -> Self { - Self { - namespace, - name, - database_bytes: 64 << 20, - capture_bytes: 16 << 20, - } - } - - pub fn with_limits(mut self, database_bytes: u64, capture_bytes: u64) -> Self { - self.database_bytes = database_bytes; - self.capture_bytes = capture_bytes; - self - } -} - -pub trait ApplicationBuilderExt { - fn module( - &mut self, - module: M, - bindings: impl IntoIterator, - ) -> cellule_runtime::Result<()>; -} - -impl ApplicationBuilderExt for ApplicationBuilder { - fn module( - &mut self, - module: M, - bindings: impl IntoIterator, - ) -> cellule_runtime::Result<()> { - let descriptor = module.descriptor(); - let cells: Vec = bindings - .into_iter() - .map(|binding| { - let namespace = descriptor - .namespaces - .iter() - .find(|namespace| namespace.id == binding.namespace) - .ok_or(Error::Registry( - "Cell binding namespace is absent from module", - ))?; - CellType::new( - M::NAME, - binding.name, - namespace.id, - namespace.role, - namespace.shards, - )? - .with_schema_range(descriptor.schema_min, descriptor.schema_max)? - .with_limits(binding.database_bytes, binding.capture_bytes) - }) - .collect::>()?; - self.register(module)?; - for cell in cells { - self.cell_type(cell)?; - } - // ApplicationBuilder::finish remains the canonical full-contract check. - Ok(()) - } -} - -pub struct ServiceNode { - application: Arc, - node: LocalNode, -} - -impl ServiceNode { - pub async fn start( - application: Arc, - store: Store, - config: NodeConfig, - provisioned_tenants: &[TenantId], - ) -> AppResult { - // LocalNode owns the only CellNode/runtime, directory enrollment, - // renewable lease and supervised task group. - let node = LocalNode::start(application.clone(), store, config).await?; - let setup: AppResult<()> = async { - for tenant in provisioned_tenants { - let app = node.application_handle::(*tenant)?; - let target = app.target_for_scope(ORDERS, b"orders")?; - node.open_cell(&target, &Orders).await?; - } - Ok(()) - } - .await; - if let Err(error) = setup { - if let Err(cleanup) = node.shutdown().await { - eprintln!("startup cleanup failed: {cleanup}"); - } - return Err(error); - } - Ok(Self { application, node }) - } - - pub fn application(&self) -> &CompiledApplication { - &self.application - } - - pub fn scope( - &self, - authorized_tenant: TenantId, - ) -> cellule_cookbook_support::Result> { - self.node.application_handle(authorized_tenant) - } - - pub fn is_ready(&self) -> bool { - self.node.is_ready() - } - - pub async fn shutdown(&self) -> cellule_cookbook_support::Result<()> { - // LocalNode drains accepted work with lease renewal still live, then - // its lease-maintenance task withdraws the latest directory generation. - self.node.shutdown().await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use cellule_runtime::{BuildDescriptor, CatalogRole, Digest}; - - #[test] - fn combined_registration_preserves_canonical_application_and_release_bytes() - -> cellule_runtime::Result<()> { - let build = BuildDescriptor { - source_revision: "builder-example-test".into(), - cargo_lock_digest: Digest::from_bytes([9; 32]), - }; - let proposed = OrdersApp::compile(build.clone())?; - let mut legacy = ApplicationBuilder::new(OrdersApp::NAME, build)?; - legacy.register(Orders)?; - legacy.cell_type( - CellType::new(Orders::NAME, "orders", ORDERS, CatalogRole::Sql, 1)? - .with_limits(64 << 20, 16 << 20)?, - )?; - let legacy = legacy.finish()?; - assert_eq!(proposed.descriptor_bytes(), legacy.descriptor_bytes()); - assert_eq!( - proposed.registry().release_bytes(), - legacy.registry().release_bytes() - ); - Ok(()) - } -} diff --git a/examples/application-builder-service/src/service.rs b/examples/application-builder-service/src/service.rs new file mode 100644 index 00000000..c3a08bae --- /dev/null +++ b/examples/application-builder-service/src/service.rs @@ -0,0 +1,63 @@ +//! Application-owned startup and provisioning policy over native framework bindings. + +use cellule_app::{ApplicationBinding, ApplicationHandle, CompiledApplication}; +use cellule_cookbook_support::{LocalNode, NodeConfig}; +use cellule_runtime::TenantId; +use cellule_store::Store; +use std::sync::Arc; + +use crate::{ + AppResult, + application::{ORDERS, Orders, OrdersApp}, +}; + +pub struct ServiceNode { + binding: ApplicationBinding, + node: LocalNode, +} + +impl ServiceNode { + pub async fn start( + application: Arc, + store: Store, + config: NodeConfig, + provisioned_tenants: &[TenantId], + ) -> AppResult { + let node = LocalNode::start(application, store, config).await?; + let setup: AppResult> = async { + let binding = node.application_binding::()?; + for tenant in provisioned_tenants { + let target = binding.scope(*tenant).target_for_scope(ORDERS, b"orders")?; + node.open_cell(&target, &Orders).await?; + } + Ok(binding) + } + .await; + let binding = match setup { + Ok(binding) => binding, + Err(error) => { + if let Err(cleanup) = node.shutdown().await { + eprintln!("startup cleanup failed: {cleanup}"); + } + return Err(error); + } + }; + Ok(Self { binding, node }) + } + + pub fn application(&self) -> &CompiledApplication { + self.binding.compiled() + } + + pub fn scope(&self, authorized_tenant: TenantId) -> ApplicationHandle { + self.binding.scope(authorized_tenant) + } + + pub fn is_ready(&self) -> bool { + self.node.is_ready() + } + + pub async fn shutdown(&self) -> cellule_cookbook_support::Result<()> { + self.node.shutdown().await + } +} From 590e658e600af9c6ad5280498962c28b5ff951cf Mon Sep 17 00:00:00 2001 From: forhappy Date: Sun, 4 Oct 2026 15:42:04 -0700 Subject: [PATCH 3/7] chore(axum): colocate application builder service example --- .github/workflows/rust.yml | 6 +++--- crates/cellule-app/README.md | 2 +- crates/cellule-app/docs/examples.md | 4 ++-- crates/cellule-axum/README.md | 18 ++++++++++++++++++ crates/cellule-axum/docs/README.md | 5 ++++- .../application-builder-service/Cargo.lock | 0 .../application-builder-service/Cargo.toml | 10 +++++----- .../application-builder-service/README.md | 18 ++++++++++++++---- .../src/application.rs | 0 .../application-builder-service/src/auth.rs | 0 .../application-builder-service/src/journal.rs | 0 .../application-builder-service/src/main.rs | 0 .../src/recovery.rs | 0 .../application-builder-service/src/service.rs | 0 crates/cellule-host/README.md | 2 +- docs/api.md | 2 +- 16 files changed, 49 insertions(+), 18 deletions(-) rename {examples => crates/cellule-axum/examples}/application-builder-service/Cargo.lock (100%) rename {examples => crates/cellule-axum/examples}/application-builder-service/Cargo.toml (69%) rename {examples => crates/cellule-axum/examples}/application-builder-service/README.md (89%) rename {examples => crates/cellule-axum/examples}/application-builder-service/src/application.rs (100%) rename {examples => crates/cellule-axum/examples}/application-builder-service/src/auth.rs (100%) rename {examples => crates/cellule-axum/examples}/application-builder-service/src/journal.rs (100%) rename {examples => crates/cellule-axum/examples}/application-builder-service/src/main.rs (100%) rename {examples => crates/cellule-axum/examples}/application-builder-service/src/recovery.rs (100%) rename {examples => crates/cellule-axum/examples}/application-builder-service/src/service.rs (100%) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2b6cad65..8b3084cc 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -52,8 +52,8 @@ jobs: run: cargo check --workspace --all-targets --all-features --locked - name: Verify application builder embedding example run: | - cargo fmt --manifest-path examples/application-builder-service/Cargo.toml --check - cargo clippy --manifest-path examples/application-builder-service/Cargo.toml --all-targets --locked -- -D warnings + cargo fmt --manifest-path crates/cellule-axum/examples/application-builder-service/Cargo.toml --check + cargo clippy --manifest-path crates/cellule-axum/examples/application-builder-service/Cargo.toml --all-targets --locked -- -D warnings - name: Documentation links run: RUSTDOCFLAGS='-D warnings' cargo doc --workspace --all-features --no-deps --locked - name: Test @@ -142,4 +142,4 @@ jobs: - name: Check Rust 1.97 minimum run: | cargo check --workspace --all-targets --all-features --locked - cargo check --manifest-path examples/application-builder-service/Cargo.toml --all-targets --locked + cargo check --manifest-path crates/cellule-axum/examples/application-builder-service/Cargo.toml --all-targets --locked diff --git a/crates/cellule-app/README.md b/crates/cellule-app/README.md index 53d3584e..b37648dd 100644 --- a/crates/cellule-app/README.md +++ b/crates/cellule-app/README.md @@ -62,7 +62,7 @@ with `ApplicationBinding::::new`. Authorize each tenant before calling `binding.scope(tenant)`. `cellule-host` provides `CellNode::bind_local_application` to create this factory from its existing runtime and a supplied storage layout. The same artifact can feed Axum/OpenAPI; -the [complete service example](../../examples/application-builder-service/README.md) +the [complete service example](../cellule-axum/examples/application-builder-service/README.md) uses these framework building blocks. | Guide | Topic | diff --git a/crates/cellule-app/docs/examples.md b/crates/cellule-app/docs/examples.md index 267b7e20..814c8576 100644 --- a/crates/cellule-app/docs/examples.md +++ b/crates/cellule-app/docs/examples.md @@ -19,8 +19,8 @@ modules + Cell types -> compiled descriptor -> catalog + fenced owner | `cargo run -p cellule-app --example schedules --locked` | Cron, Effects | Register a fixed-interval schedule, drive one due maintenance tick, deliver its effect through a signed local peer loopback, and count the destination row. | For a complete HTTP service, run -`cargo run --manifest-path examples/application-builder-service/Cargo.toml --locked`. -The [service guide](../../../examples/application-builder-service/README.md) +`cargo run --manifest-path crates/cellule-axum/examples/application-builder-service/Cargo.toml --locked`. +The [service guide](../../cellule-axum/examples/application-builder-service/README.md) uses native module registration and scoped factories, authorized Axum routes, OpenAPI, retained command evidence, and ordered host shutdown. diff --git a/crates/cellule-axum/README.md b/crates/cellule-axum/README.md index 321b8a77..a5de89f3 100644 --- a/crates/cellule-axum/README.md +++ b/crates/cellule-axum/README.md @@ -17,6 +17,24 @@ tenant/resource authorization, providers, listener, and shutdown. The adapter uses that handle; the runtime owns command execution, durable publication, and receipts. +| Runnable example | Focus | Default port | +| --- | --- | --- | +| [`sql`](examples/sql.rs) | Manual REST handlers and receipt-bound reads. | 3000 | +| [`integration`](examples/integration.rs) | Typed routes, authorized context, OpenAPI, and recovery journal. | 3001 | +| [`application-builder-service`](examples/application-builder-service/README.md) | Native module registration, shared scoped factory, two tenants, leased host startup, and ordered shutdown. | 3002 | + +For the full application-to-host integration, run the standalone service from +the workspace root: + +```sh +cargo run --manifest-path crates/cellule-axum/examples/application-builder-service/Cargo.toml --locked +``` + +Its HTTP layer uses Axum/OpenAPI; the `CellBinding`, compilation, and +`ApplicationBinding` APIs are reusable with other adapters and workers. It keeps +an independent manifest and lockfile so application-owned cookbook startup +dependencies stay outside the adapter crate. + ## Try the complete integration Run this from the Cellule workspace: diff --git a/crates/cellule-axum/docs/README.md b/crates/cellule-axum/docs/README.md index 561df217..8a1b8456 100644 --- a/crates/cellule-axum/docs/README.md +++ b/crates/cellule-axum/docs/README.md @@ -11,6 +11,7 @@ Signed node transport uses the separate | --- | --- | | Fixed tenant with application-written handlers | [`sql` example](../examples/sql.rs) | | Authorized request context, typed registration and OpenAPI | [`integration` example](../examples/integration.rs) | +| Native application builder, tenant scopes, leased host startup and shutdown | [Standalone application service](../examples/application-builder-service/README.md) | | Atomic evidence storage | [Example journal](../examples/recovery/mod.rs) | | Local provider setup and cleanup on failure | [Example support](../examples/support/mod.rs) | | Durable runtime snapshot and resolution contracts | [Uncertain command guide](../../../docs/api.md#handle-an-uncertain-command) | @@ -20,7 +21,9 @@ Signed node transport uses the separate 1. Authenticate the session/token using application middleware. 2. Look up its permitted tenant and application. Authorize the route's action and resource; derive its Cell target using the compiled topology. -3. Construct/select the corresponding `ApplicationHandle` and install +3. Create the corresponding `ApplicationHandle` with a trusted + `ApplicationBinding::scope(authorized_tenant)`, or select an existing + scoped handle, and install it into request extensions. Keep any authorized actor/resource context alongside it; a handle is a capability, not a principal. 4. Extract `RequestCellule` in a manual handler, or implement diff --git a/examples/application-builder-service/Cargo.lock b/crates/cellule-axum/examples/application-builder-service/Cargo.lock similarity index 100% rename from examples/application-builder-service/Cargo.lock rename to crates/cellule-axum/examples/application-builder-service/Cargo.lock diff --git a/examples/application-builder-service/Cargo.toml b/crates/cellule-axum/examples/application-builder-service/Cargo.toml similarity index 69% rename from examples/application-builder-service/Cargo.toml rename to crates/cellule-axum/examples/application-builder-service/Cargo.toml index 2f20882a..5def077e 100644 --- a/examples/application-builder-service/Cargo.toml +++ b/crates/cellule-axum/examples/application-builder-service/Cargo.toml @@ -12,11 +12,11 @@ resolver = "3" [dependencies] axum = { version = "0.8.9", features = ["json", "http1", "tokio"] } blake3 = "1.8" -cellule-app = { path = "../../crates/cellule-app" } -cellule-axum = { path = "../../crates/cellule-axum", features = ["openapi"] } -cellule-cookbook-support = { path = "../../cookbook/support" } -cellule-runtime = { path = "../../crates/cellule-runtime" } -cellule-store = { path = "../../crates/cellule-store" } +cellule-app = { path = "../../../cellule-app" } +cellule-axum = { path = "../..", features = ["openapi"] } +cellule-cookbook-support = { path = "../../../../cookbook/support" } +cellule-runtime = { path = "../../../cellule-runtime" } +cellule-store = { path = "../../../cellule-store" } object_store = { version = "0.14.1", default-features = false } rusqlite = { version = "0.34", features = ["bundled"] } serde_json = "1" diff --git a/examples/application-builder-service/README.md b/crates/cellule-axum/examples/application-builder-service/README.md similarity index 89% rename from examples/application-builder-service/README.md rename to crates/cellule-axum/examples/application-builder-service/README.md index 7a53889c..c6ecf462 100644 --- a/examples/application-builder-service/README.md +++ b/crates/cellule-axum/examples/application-builder-service/README.md @@ -1,12 +1,22 @@ -# Application builder → Axum service +# Application builder → Axum/OpenAPI service A complete embedding application using native Cellule registration and scoped handle factories. `CellBinding`, `ApplicationBuilder::module`, and -`ApplicationBinding` belong to [cellule-app](../../crates/cellule-app/README.md); -the local binding method belongs to [cellule-host](../../crates/cellule-host/README.md). +`ApplicationBinding` belong to [cellule-app](../../../cellule-app/README.md); +the local binding method belongs to [cellule-host](../../../cellule-host/README.md). Application-owned startup and tenant provisioning remain in [service.rs](src/service.rs), using the existing cookbook host assembly. +The HTTP service is specific to Axum/OpenAPI. Module declaration, application +compilation, `ApplicationBinding::scope`, and host binding are framework APIs +that other HTTP adapters, workers, or command-line applications can use. The +Axum-specific pieces are the request extractors, `CellApi` routes, OpenAPI +annotations, and HTTP listener/shutdown wiring. + +This example keeps its own `Cargo.toml` and lockfile as an embedding application. +Run it with `--manifest-path`; the adapter's `sql` and `integration` examples +remain ordinary `cargo run -p cellule-axum --example ...` targets. + ```text Orders module + Cell binding │ @@ -51,7 +61,7 @@ application identity, authorization, HTTP routes, listener, and evidence journal From the repository root: ```sh -cargo run --manifest-path examples/application-builder-service/Cargo.toml --locked +cargo run --manifest-path crates/cellule-axum/examples/application-builder-service/Cargo.toml --locked ``` The server binds `127.0.0.1:3002`. Set `CELLULE_EXAMPLE_BIND=127.0.0.1:0` diff --git a/examples/application-builder-service/src/application.rs b/crates/cellule-axum/examples/application-builder-service/src/application.rs similarity index 100% rename from examples/application-builder-service/src/application.rs rename to crates/cellule-axum/examples/application-builder-service/src/application.rs diff --git a/examples/application-builder-service/src/auth.rs b/crates/cellule-axum/examples/application-builder-service/src/auth.rs similarity index 100% rename from examples/application-builder-service/src/auth.rs rename to crates/cellule-axum/examples/application-builder-service/src/auth.rs diff --git a/examples/application-builder-service/src/journal.rs b/crates/cellule-axum/examples/application-builder-service/src/journal.rs similarity index 100% rename from examples/application-builder-service/src/journal.rs rename to crates/cellule-axum/examples/application-builder-service/src/journal.rs diff --git a/examples/application-builder-service/src/main.rs b/crates/cellule-axum/examples/application-builder-service/src/main.rs similarity index 100% rename from examples/application-builder-service/src/main.rs rename to crates/cellule-axum/examples/application-builder-service/src/main.rs diff --git a/examples/application-builder-service/src/recovery.rs b/crates/cellule-axum/examples/application-builder-service/src/recovery.rs similarity index 100% rename from examples/application-builder-service/src/recovery.rs rename to crates/cellule-axum/examples/application-builder-service/src/recovery.rs diff --git a/examples/application-builder-service/src/service.rs b/crates/cellule-axum/examples/application-builder-service/src/service.rs similarity index 100% rename from examples/application-builder-service/src/service.rs rename to crates/cellule-axum/examples/application-builder-service/src/service.rs diff --git a/crates/cellule-host/README.md b/crates/cellule-host/README.md index e01d5edf..3c00be1e 100644 --- a/crates/cellule-host/README.md +++ b/crates/cellule-host/README.md @@ -12,7 +12,7 @@ acquires no Cell, and leaves readiness and drain with the original node. `bind_application` accepts an already configured client, including an application-owned remote transport. Both paths reject mismatched application types and client registries. See the -[complete embedding service](../../examples/application-builder-service/README.md) +[complete embedding service](../cellule-axum/examples/application-builder-service/README.md) for scoped Axum routes and OpenAPI using these bindings. ```text diff --git a/docs/api.md b/docs/api.md index 8649a5d1..0a2df696 100644 --- a/docs/api.md +++ b/docs/api.md @@ -117,7 +117,7 @@ still rejects targets outside its tenant, application, namespace, or declared partition scheme. `ApplicationHandle::new` remains available for directly binding one tenant through the same validation path. Binding alone does not activate Cells; the host must provision the catalog and establish an owner before calls. -The [embedding service](../examples/application-builder-service/README.md) uses +The [embedding service](../crates/cellule-axum/examples/application-builder-service/README.md) uses this flow with Axum/OpenAPI and ordered shutdown. Use `target_for_scope(namespace, scope)` to derive a target from the declared From 2c2e164818de41c28202dc8513e972f2322202d4 Mon Sep 17 00:00:00 2001 From: forhappy Date: Sun, 4 Oct 2026 15:51:22 -0700 Subject: [PATCH 4/7] chore(axum): group original integration example in its own folder --- crates/cellule-axum/Cargo.toml | 2 +- crates/cellule-axum/README.md | 24 +++++++++++++++---- crates/cellule-axum/docs/README.md | 6 ++--- .../{integration.rs => integration/main.rs} | 0 .../{ => integration}/recovery/mod.rs | 0 .../examples/{ => integration}/support/mod.rs | 2 +- 6 files changed, 25 insertions(+), 9 deletions(-) rename crates/cellule-axum/examples/{integration.rs => integration/main.rs} (100%) rename crates/cellule-axum/examples/{ => integration}/recovery/mod.rs (100%) rename crates/cellule-axum/examples/{ => integration}/support/mod.rs (99%) diff --git a/crates/cellule-axum/Cargo.toml b/crates/cellule-axum/Cargo.toml index f1eaedcc..68be06dd 100644 --- a/crates/cellule-axum/Cargo.toml +++ b/crates/cellule-axum/Cargo.toml @@ -35,5 +35,5 @@ tower = { version = "0.5", features = ["util"] } [[example]] name = "integration" -path = "examples/integration.rs" +path = "examples/integration/main.rs" required-features = ["openapi"] diff --git a/crates/cellule-axum/README.md b/crates/cellule-axum/README.md index a5de89f3..8cdfc93c 100644 --- a/crates/cellule-axum/README.md +++ b/crates/cellule-axum/README.md @@ -20,9 +20,25 @@ and receipts. | Runnable example | Focus | Default port | | --- | --- | --- | | [`sql`](examples/sql.rs) | Manual REST handlers and receipt-bound reads. | 3000 | -| [`integration`](examples/integration.rs) | Typed routes, authorized context, OpenAPI, and recovery journal. | 3001 | +| [`integration`](examples/integration/main.rs) | Typed routes, authorized context, OpenAPI, and recovery journal. | 3001 | | [`application-builder-service`](examples/application-builder-service/README.md) | Native module registration, shared scoped factory, two tenants, leased host startup, and ordered shutdown. | 3002 | +The complete service examples have separate folders under `examples/`: + +```text +examples/ +├── sql.rs # manual REST and performance service +├── sql_metrics/ # SQL service telemetry +├── http_load.rs # HTTP read load driver +├── integration/ # original adapter integration +│ ├── main.rs +│ ├── support/mod.rs # local module and runtime setup +│ └── recovery/mod.rs # command evidence journal +└── application-builder-service/ # native builder and leased host integration + ├── Cargo.toml + └── src/ +``` + For the full application-to-host integration, run the standalone service from the workspace root: @@ -43,7 +59,7 @@ Run this from the Cellule workspace: cargo run -p cellule-axum --example integration --features openapi --locked ``` -The [integration example](examples/integration.rs) binds `127.0.0.1:3001`. +The [integration example](examples/integration/main.rs) binds `127.0.0.1:3001`. It includes an authorized extractor, a SQLite recovery journal, typed routes, and an OpenAPI document. @@ -222,8 +238,8 @@ Implement `X` in your application: after both are durable; retention failure prevents dispatch. Identical evidence must be idempotent and conflicting bytes must be refused. -The [example extractor](examples/integration.rs) and -[SQLite journal](examples/recovery/mod.rs) implement these contracts. Authenticate +The [example extractor](examples/integration/main.rs) and +[SQLite journal](examples/integration/recovery/mod.rs) implement these contracts. Authenticate in the extractor, or read an authorized context installed by middleware. ```mermaid diff --git a/crates/cellule-axum/docs/README.md b/crates/cellule-axum/docs/README.md index 8a1b8456..69d4767d 100644 --- a/crates/cellule-axum/docs/README.md +++ b/crates/cellule-axum/docs/README.md @@ -10,10 +10,10 @@ Signed node transport uses the separate | Need | Route | | --- | --- | | Fixed tenant with application-written handlers | [`sql` example](../examples/sql.rs) | -| Authorized request context, typed registration and OpenAPI | [`integration` example](../examples/integration.rs) | +| Authorized request context, typed registration and OpenAPI | [`integration` example](../examples/integration/main.rs) | | Native application builder, tenant scopes, leased host startup and shutdown | [Standalone application service](../examples/application-builder-service/README.md) | -| Atomic evidence storage | [Example journal](../examples/recovery/mod.rs) | -| Local provider setup and cleanup on failure | [Example support](../examples/support/mod.rs) | +| Atomic evidence storage | [Example journal](../examples/integration/recovery/mod.rs) | +| Local provider setup and cleanup on failure | [Example support](../examples/integration/support/mod.rs) | | Durable runtime snapshot and resolution contracts | [Uncertain command guide](../../../docs/api.md#handle-an-uncertain-command) | ## Request scope in a multi-tenant service diff --git a/crates/cellule-axum/examples/integration.rs b/crates/cellule-axum/examples/integration/main.rs similarity index 100% rename from crates/cellule-axum/examples/integration.rs rename to crates/cellule-axum/examples/integration/main.rs diff --git a/crates/cellule-axum/examples/recovery/mod.rs b/crates/cellule-axum/examples/integration/recovery/mod.rs similarity index 100% rename from crates/cellule-axum/examples/recovery/mod.rs rename to crates/cellule-axum/examples/integration/recovery/mod.rs diff --git a/crates/cellule-axum/examples/support/mod.rs b/crates/cellule-axum/examples/integration/support/mod.rs similarity index 99% rename from crates/cellule-axum/examples/support/mod.rs rename to crates/cellule-axum/examples/integration/support/mod.rs index b94874a8..69e41113 100644 --- a/crates/cellule-axum/examples/support/mod.rs +++ b/crates/cellule-axum/examples/integration/support/mod.rs @@ -162,7 +162,7 @@ pub async fn start() -> ExampleResult { let application = Arc::new(OrdersApp::compile(BuildDescriptor { source_revision: "local-axum-orders-example".into(), cargo_lock_digest: Digest::from_bytes( - *blake3::hash(include_bytes!("../../../../Cargo.lock")).as_bytes(), + *blake3::hash(include_bytes!("../../../../../Cargo.lock")).as_bytes(), ), })?); let tenant = TenantId::from_bytes([2; 16]); From 2ec3c375885f6c4861238641a958c1389e1115ae Mon Sep 17 00:00:00 2001 From: forhappy Date: Sun, 4 Oct 2026 16:05:38 -0700 Subject: [PATCH 5/7] chore(axum): rename integration example to typed-api-service --- crates/cellule-axum/Cargo.toml | 4 ++-- crates/cellule-axum/README.md | 14 +++++++------- crates/cellule-axum/docs/README.md | 6 +++--- .../examples/application-builder-service/README.md | 2 +- .../{integration => typed-api-service}/main.rs | 4 ++-- .../recovery/mod.rs | 0 .../support/mod.rs | 0 7 files changed, 15 insertions(+), 15 deletions(-) rename crates/cellule-axum/examples/{integration => typed-api-service}/main.rs (98%) rename crates/cellule-axum/examples/{integration => typed-api-service}/recovery/mod.rs (100%) rename crates/cellule-axum/examples/{integration => typed-api-service}/support/mod.rs (100%) diff --git a/crates/cellule-axum/Cargo.toml b/crates/cellule-axum/Cargo.toml index 68be06dd..95312679 100644 --- a/crates/cellule-axum/Cargo.toml +++ b/crates/cellule-axum/Cargo.toml @@ -34,6 +34,6 @@ tokio = { workspace = true, features = ["macros", "net", "rt-multi-thread", "sig tower = { version = "0.5", features = ["util"] } [[example]] -name = "integration" -path = "examples/integration/main.rs" +name = "typed-api-service" +path = "examples/typed-api-service/main.rs" required-features = ["openapi"] diff --git a/crates/cellule-axum/README.md b/crates/cellule-axum/README.md index 8cdfc93c..05be7f49 100644 --- a/crates/cellule-axum/README.md +++ b/crates/cellule-axum/README.md @@ -20,7 +20,7 @@ and receipts. | Runnable example | Focus | Default port | | --- | --- | --- | | [`sql`](examples/sql.rs) | Manual REST handlers and receipt-bound reads. | 3000 | -| [`integration`](examples/integration/main.rs) | Typed routes, authorized context, OpenAPI, and recovery journal. | 3001 | +| [`typed-api-service`](examples/typed-api-service/main.rs) | Typed routes, authorized context, OpenAPI, and recovery journal. | 3001 | | [`application-builder-service`](examples/application-builder-service/README.md) | Native module registration, shared scoped factory, two tenants, leased host startup, and ordered shutdown. | 3002 | The complete service examples have separate folders under `examples/`: @@ -30,7 +30,7 @@ examples/ ├── sql.rs # manual REST and performance service ├── sql_metrics/ # SQL service telemetry ├── http_load.rs # HTTP read load driver -├── integration/ # original adapter integration +├── typed-api-service/ # typed routes, authorization and OpenAPI │ ├── main.rs │ ├── support/mod.rs # local module and runtime setup │ └── recovery/mod.rs # command evidence journal @@ -51,15 +51,15 @@ Its HTTP layer uses Axum/OpenAPI; the `CellBinding`, compilation, and an independent manifest and lockfile so application-owned cookbook startup dependencies stay outside the adapter crate. -## Try the complete integration +## Try the typed API service Run this from the Cellule workspace: ```sh -cargo run -p cellule-axum --example integration --features openapi --locked +cargo run -p cellule-axum --example typed-api-service --features openapi --locked ``` -The [integration example](examples/integration/main.rs) binds `127.0.0.1:3001`. +The [typed API service](examples/typed-api-service/main.rs) binds `127.0.0.1:3001`. It includes an authorized extractor, a SQLite recovery journal, typed routes, and an OpenAPI document. @@ -238,8 +238,8 @@ Implement `X` in your application: after both are durable; retention failure prevents dispatch. Identical evidence must be idempotent and conflicting bytes must be refused. -The [example extractor](examples/integration/main.rs) and -[SQLite journal](examples/integration/recovery/mod.rs) implement these contracts. Authenticate +The [example extractor](examples/typed-api-service/main.rs) and +[SQLite journal](examples/typed-api-service/recovery/mod.rs) implement these contracts. Authenticate in the extractor, or read an authorized context installed by middleware. ```mermaid diff --git a/crates/cellule-axum/docs/README.md b/crates/cellule-axum/docs/README.md index 69d4767d..053ed44c 100644 --- a/crates/cellule-axum/docs/README.md +++ b/crates/cellule-axum/docs/README.md @@ -10,10 +10,10 @@ Signed node transport uses the separate | Need | Route | | --- | --- | | Fixed tenant with application-written handlers | [`sql` example](../examples/sql.rs) | -| Authorized request context, typed registration and OpenAPI | [`integration` example](../examples/integration/main.rs) | +| Authorized request context, typed registration and OpenAPI | [`typed-api-service` example](../examples/typed-api-service/main.rs) | | Native application builder, tenant scopes, leased host startup and shutdown | [Standalone application service](../examples/application-builder-service/README.md) | -| Atomic evidence storage | [Example journal](../examples/integration/recovery/mod.rs) | -| Local provider setup and cleanup on failure | [Example support](../examples/integration/support/mod.rs) | +| Atomic evidence storage | [Example journal](../examples/typed-api-service/recovery/mod.rs) | +| Local provider setup and cleanup on failure | [Example support](../examples/typed-api-service/support/mod.rs) | | Durable runtime snapshot and resolution contracts | [Uncertain command guide](../../../docs/api.md#handle-an-uncertain-command) | ## Request scope in a multi-tenant service diff --git a/crates/cellule-axum/examples/application-builder-service/README.md b/crates/cellule-axum/examples/application-builder-service/README.md index c6ecf462..88c34c02 100644 --- a/crates/cellule-axum/examples/application-builder-service/README.md +++ b/crates/cellule-axum/examples/application-builder-service/README.md @@ -14,7 +14,7 @@ Axum-specific pieces are the request extractors, `CellApi` routes, OpenAPI annotations, and HTTP listener/shutdown wiring. This example keeps its own `Cargo.toml` and lockfile as an embedding application. -Run it with `--manifest-path`; the adapter's `sql` and `integration` examples +Run it with `--manifest-path`; the adapter's `sql` and `typed-api-service` examples remain ordinary `cargo run -p cellule-axum --example ...` targets. ```text diff --git a/crates/cellule-axum/examples/integration/main.rs b/crates/cellule-axum/examples/typed-api-service/main.rs similarity index 98% rename from crates/cellule-axum/examples/integration/main.rs rename to crates/cellule-axum/examples/typed-api-service/main.rs index 04eaaa3d..c38ddeeb 100644 --- a/crates/cellule-axum/examples/integration/main.rs +++ b/crates/cellule-axum/examples/typed-api-service/main.rs @@ -1,5 +1,5 @@ //! Typed registration, authorization, OpenAPI, evidence custody and recovery. -//! Run: cargo run -p cellule-axum --example integration --features openapi --locked +//! Run: cargo run -p cellule-axum --example typed-api-service --features openapi --locked mod recovery; mod support; @@ -223,7 +223,7 @@ async fn main() -> ExampleResult<()> { .with_state(state); let listener = tokio::net::TcpListener::bind("127.0.0.1:3001").await?; println!( - "Integration service: http://{} (Ctrl-C to drain)", + "Typed API service: http://{} (Ctrl-C to drain)", listener.local_addr()? ); let (signal_tx, signal_rx) = tokio::sync::oneshot::channel(); diff --git a/crates/cellule-axum/examples/integration/recovery/mod.rs b/crates/cellule-axum/examples/typed-api-service/recovery/mod.rs similarity index 100% rename from crates/cellule-axum/examples/integration/recovery/mod.rs rename to crates/cellule-axum/examples/typed-api-service/recovery/mod.rs diff --git a/crates/cellule-axum/examples/integration/support/mod.rs b/crates/cellule-axum/examples/typed-api-service/support/mod.rs similarity index 100% rename from crates/cellule-axum/examples/integration/support/mod.rs rename to crates/cellule-axum/examples/typed-api-service/support/mod.rs From bd7856b7303a7138acaf8e12b0f736b79ab0664d Mon Sep 17 00:00:00 2001 From: forhappy Date: Sun, 4 Oct 2026 16:21:22 -0700 Subject: [PATCH 6/7] docs(axum): serve interactive OpenAPI docs in service examples --- crates/cellule-axum/README.md | 17 ++++- crates/cellule-axum/docs/README.md | 1 + .../application-builder-service/README.md | 8 +++ .../application-builder-service/src/main.rs | 19 +++++- crates/cellule-axum/examples/openapi-ui.html | 29 +++++++++ .../examples/typed-api-service/main.rs | 65 +++++++++++++++++-- 6 files changed, 129 insertions(+), 10 deletions(-) create mode 100644 crates/cellule-axum/examples/openapi-ui.html diff --git a/crates/cellule-axum/README.md b/crates/cellule-axum/README.md index 05be7f49..53d5d813 100644 --- a/crates/cellule-axum/README.md +++ b/crates/cellule-axum/README.md @@ -30,6 +30,7 @@ examples/ ├── sql.rs # manual REST and performance service ├── sql_metrics/ # SQL service telemetry ├── http_load.rs # HTTP read load driver +├── openapi-ui.html # shared interactive docs page ├── typed-api-service/ # typed routes, authorization and OpenAPI │ ├── main.rs │ ├── support/mod.rs # local module and runtime setup @@ -63,6 +64,19 @@ The [typed API service](examples/typed-api-service/main.rs) binds `127.0.0.1:300 It includes an authorized extractor, a SQLite recovery journal, typed routes, and an OpenAPI document. +Open [interactive API docs](http://127.0.0.1:3001/docs) in your browser. Select +**Authorize**, enter `local-orders` without the `Bearer` prefix, then use +**Try it out**. For `POST /total/read`, send JSON `null`. To write a total, use +the mutation body generated by the curl recipe below; it supplies a fresh UUID +and valid timestamps. Preserve that body for retries. + +The examples serve the same generated `/openapi.json` document to Swagger UI +and SDK generators. The [shared page](examples/openapi-ui.html) loads pinned +Swagger UI 5.32.0 assets from unpkg, so the browser needs internet access. +Applications can serve those assets locally using the +[Swagger UI installation guide](https://swagger.io/docs/open-source-tools/swagger-ui/usage/installation/). +Documentation routes and access control remain application-owned. + | Route | Request | Result | | --- | --- | --- | | `POST /total` | Mutation identity + integer input | `200`, committed total + receipt | @@ -70,7 +84,8 @@ and an OpenAPI document. | `POST /recovery/{request_id}` | Original request ID | Resolve a retained command in the authorized scope | | `GET /scope` | Authorized request | Selected Cell identity | | `GET /ready` | No credentials | `204` when the initialized Cell can answer a query | -| `GET /openapi.json` | No credentials | OpenAPI for the generated total routes | +| `GET /docs` | No credentials | Interactive Swagger UI with authorization and request execution | +| `GET /openapi.json` | No credentials | OpenAPI for total, recovery, scope, and readiness routes | In a second terminal, create one mutation body, write it, and read at its receipt: diff --git a/crates/cellule-axum/docs/README.md b/crates/cellule-axum/docs/README.md index 053ed44c..77c7b908 100644 --- a/crates/cellule-axum/docs/README.md +++ b/crates/cellule-axum/docs/README.md @@ -11,6 +11,7 @@ Signed node transport uses the separate | --- | --- | | Fixed tenant with application-written handlers | [`sql` example](../examples/sql.rs) | | Authorized request context, typed registration and OpenAPI | [`typed-api-service` example](../examples/typed-api-service/main.rs) | +| Interactive API documentation | [Application-owned Swagger UI page](../examples/openapi-ui.html), served at `/docs` by both service examples | | Native application builder, tenant scopes, leased host startup and shutdown | [Standalone application service](../examples/application-builder-service/README.md) | | Atomic evidence storage | [Example journal](../examples/typed-api-service/recovery/mod.rs) | | Local provider setup and cleanup on failure | [Example support](../examples/typed-api-service/support/mod.rs) | diff --git a/crates/cellule-axum/examples/application-builder-service/README.md b/crates/cellule-axum/examples/application-builder-service/README.md index 88c34c02..5bcabe24 100644 --- a/crates/cellule-axum/examples/application-builder-service/README.md +++ b/crates/cellule-axum/examples/application-builder-service/README.md @@ -67,6 +67,13 @@ cargo run --manifest-path crates/cellule-axum/examples/application-builder-servi The server binds `127.0.0.1:3002`. Set `CELLULE_EXAMPLE_BIND=127.0.0.1:0` to have the OS choose a free port; the selected address is printed. +Open [interactive API docs](http://127.0.0.1:3002/docs), select **Authorize**, +and enter `alpha-writer` or `beta-reader` without the `Bearer` prefix. Use +**Try it out** to call routes; the read body is JSON `null`. The write recipe +below generates a valid mutation identity. The [shared Swagger UI page](../openapi-ui.html) +loads pinned assets from unpkg and needs browser internet access; the specification +at `/openapi.json` is served locally and is also suitable for SDK generation. + On a workstation with the mounted Workspace volume, set `CARGO_TARGET_DIR` to a directory unique to this checkout beneath `$HOME/Workspace/crabbuild-target`. @@ -96,6 +103,7 @@ path parameters, and mutation inputs cannot select another tenant. | `POST /orders/total/read` | JSON `null` | Observed total and receipt; accepts `x-cellule-receipt` | | `POST /orders/total/resolve/{request_id}` | Empty | Original authorized command outcome, safe replay, or unresolved state | | `GET /ready` | Empty | `204` when the node/lease are ready | +| `GET /docs` | Empty | Interactive Swagger UI with authorization and request execution | | `GET /openapi.json` | Empty | Generated command/query schemas, recovery/readiness routes, and bearer security | Write Alpha's total, retain the original body, then read at its receipt: diff --git a/crates/cellule-axum/examples/application-builder-service/src/main.rs b/crates/cellule-axum/examples/application-builder-service/src/main.rs index 2d8648c5..af165522 100644 --- a/crates/cellule-axum/examples/application-builder-service/src/main.rs +++ b/crates/cellule-axum/examples/application-builder-service/src/main.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use axum::{ Json, Router, extract::DefaultBodyLimit, + response::Html, routing::{get, post}, }; use cellule_app::CellApplication; @@ -40,17 +41,22 @@ async fn serve(state: Arc) -> AppResult<()> { CellApi::>::new(state.node.application())? .command::( ORDERS, - EndpointSpec::new("/orders/total", "setTotal"), + EndpointSpec::new("/orders/total", "setTotal") + .description("Set the authorized tenant's total with a caller-created mutation identity. Retain the exact original body for retries and recovery. Negative totals are durable rejections."), )? .query::( ORDERS, - EndpointSpec::new("/orders/total/read", "readTotal"), + EndpointSpec::new("/orders/total/read", "readTotal") + .description("Read the authorized tenant's total using JSON null as the body. Supply x-cellule-receipt to require observation of a previous write."), )? .into_router() .split_for_parts(); document.merge(ManualRoutes::openapi()); document.info.title = "Orders service".into(); document.info.version = env!("CARGO_PKG_VERSION").into(); + document.info.description = Some( + "Local example: use Authorize with token `alpha-writer` for read/write or `beta-reader` for read only (without the Bearer prefix). Queries take JSON `null`. Mutations require a UUID request_id and current issued_at_ms/expires_at_ms Unix timestamps in milliseconds; use a 60-second window and keep the same identity and input for every retry. Restarting this example resets its state.".into(), + ); // The same service that authenticates requests documents its security. use utoipa::openapi::{ @@ -91,13 +97,20 @@ async fn serve(state: Arc) -> AppResult<()> { post(recovery::resolve), ) .route("/ready", get(auth::ready)) + .route( + "/docs", + get(|| async { Html(include_str!("../../openapi-ui.html")) }), + ) .route("/openapi.json", get(move || async move { Json(document) })) .layer(DefaultBodyLimit::max(4096)) .with_state(state); let bind = std::env::var("CELLULE_EXAMPLE_BIND").unwrap_or_else(|_| "127.0.0.1:3002".into()); let listener = tokio::net::TcpListener::bind(bind).await?; - println!("Orders service: http://{}", listener.local_addr()?); + println!( + "Orders service: http://{} (API docs: /docs)", + listener.local_addr()? + ); let (signal_tx, signal_rx) = tokio::sync::oneshot::channel(); axum::serve(listener, router) .with_graceful_shutdown(async move { diff --git a/crates/cellule-axum/examples/openapi-ui.html b/crates/cellule-axum/examples/openapi-ui.html new file mode 100644 index 00000000..f5939d21 --- /dev/null +++ b/crates/cellule-axum/examples/openapi-ui.html @@ -0,0 +1,29 @@ + + + + + + Cellule API documentation + + + + + + + + + diff --git a/crates/cellule-axum/examples/typed-api-service/main.rs b/crates/cellule-axum/examples/typed-api-service/main.rs index c38ddeeb..4bff40b3 100644 --- a/crates/cellule-axum/examples/typed-api-service/main.rs +++ b/crates/cellule-axum/examples/typed-api-service/main.rs @@ -9,12 +9,13 @@ use axum::{ extract::{DefaultBodyLimit, FromRequestParts, Path, Request, State}, http::{StatusCode, request::Parts}, middleware::{self, Next}, - response::{IntoResponse, Response}, + response::{Html, IntoResponse, Response}, routing::{get, post}, }; use cellule_app::ApplicationHandle; use cellule_axum::{ CellApi, CellEndpoint, CellJson, CommandEndpoint, EndpointSpec, HttpError, RequestCellule, + utoipa, }; use cellule_runtime::{ CellTarget, PreparedCommand, Receipt, Resolution, @@ -25,8 +26,18 @@ use cellule_runtime::{ use recovery::Journal; use serde_json::json; use support::{ExampleResult, ORDERS, OrdersApp, ReadTotal, SetTotal}; +use utoipa::OpenApi as _; use uuid::Uuid; +#[derive(utoipa::OpenApi)] +#[openapi(paths(scope, recover, ready))] +struct ManualRoutes; + +#[derive(serde::Serialize, utoipa::ToSchema)] +struct AuthorizedScope { + cell: [u8; 32], +} + #[derive(Clone)] struct ServiceState { app: ApplicationHandle, @@ -92,11 +103,32 @@ impl CommandEndpoint for Authorized { } } -async fn scope(app: RequestCellule) -> Result, HttpError> { +#[utoipa::path( + get, path = "/scope", operation_id = "authorizedScope", + responses( + (status = 200, body = AuthorizedScope, description = "Cell selected by the authenticated scope"), + (status = 401, description = "Application authentication failed") + ), + security(("bearerAuth" = [])) +)] +async fn scope(app: RequestCellule) -> Result, HttpError> { let target = app.target_for_scope(ORDERS, b"orders")?; - Ok(Json(json!({"cell": target.cell_id().as_bytes()}))) + Ok(Json(AuthorizedScope { + cell: *target.cell_id().as_bytes(), + })) } +#[utoipa::path( + post, path = "/recovery/{request_id}", operation_id = "recoverTotal", + params(("request_id" = Uuid, Path, description = "Original command request ID")), + responses( + (status = 200, body = CellJson, description = "Committed original outcome or safely replayed command"), + (status = 404, description = "No retained evidence in the authorized Cell"), + (status = 409, description = "Durable rejection, request conflict, or expired original command"), + (status = 503, description = "Outcome remains unknown or the node is unavailable") + ), + security(("bearerAuth" = [])) +)] async fn recover(context: Authorized, Path(request): Path) -> Result { let Some((snapshot, input)) = context .journal @@ -155,6 +187,13 @@ async fn recover(context: Authorized, Path(request): Path) -> Result) -> StatusCode { let Ok(target) = state.app.target_for_scope(ORDERS, b"orders") else { return StatusCode::SERVICE_UNAVAILABLE; @@ -174,10 +213,23 @@ async fn main() -> ExampleResult<()> { journal: Journal::open(node._files.path().join("recovery.sqlite"))?, }; let (routes, mut document) = CellApi::::new(node.app.compiled())? - .command::(ORDERS, EndpointSpec::new("/total", "setTotal"))? - .query::(ORDERS, EndpointSpec::new("/total/read", "readTotal"))? + .command::( + ORDERS, + EndpointSpec::new("/total", "setTotal") + .description("Set the total with a caller-created mutation identity. Retain the exact original body for retries and recovery. Negative totals are durable rejections."), + )? + .query::( + ORDERS, + EndpointSpec::new("/total/read", "readTotal") + .description("Read the total using JSON null as the body. Supply x-cellule-receipt to require observation of a previous write."), + )? .into_router() .split_for_parts(); + document.merge(ManualRoutes::openapi()); + document.info.title = "Typed Orders API".into(); + document.info.description = Some( + "Local example: use Authorize with token `local-orders` (without the Bearer prefix). Queries take JSON `null`. Mutations require a UUID request_id and current issued_at_ms/expires_at_ms Unix timestamps in milliseconds; use a 60-second window and keep the same identity and input for every retry. Restarting this example resets its state.".into(), + ); // Authorization metadata belongs to the app, beside its middleware. use cellule_axum::utoipa::openapi::{ response::ResponseBuilder, @@ -212,6 +264,7 @@ async fn main() -> ExampleResult<()> { let router = Router::new() .merge(protected) .route("/ready", get(ready)) + .route("/docs", get(|| async { Html(include_str!("../openapi-ui.html")) })) .route( "/openapi.json", get(move || { @@ -223,7 +276,7 @@ async fn main() -> ExampleResult<()> { .with_state(state); let listener = tokio::net::TcpListener::bind("127.0.0.1:3001").await?; println!( - "Typed API service: http://{} (Ctrl-C to drain)", + "Typed API service: http://{} (API docs: /docs; Ctrl-C to drain)", listener.local_addr()? ); let (signal_tx, signal_rx) = tokio::sync::oneshot::channel(); From e6165afad94a9b9de9a477c143e9e3748c9fde76 Mon Sep 17 00:00:00 2001 From: forhappy Date: Mon, 5 Oct 2026 09:20:56 -0700 Subject: [PATCH 7/7] Update README.md --- .../examples/application-builder-service/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/cellule-axum/examples/application-builder-service/README.md b/crates/cellule-axum/examples/application-builder-service/README.md index 5bcabe24..8a49a8fb 100644 --- a/crates/cellule-axum/examples/application-builder-service/README.md +++ b/crates/cellule-axum/examples/application-builder-service/README.md @@ -25,9 +25,9 @@ OrdersApp::compile ───────────────► CellApi + Op │ │ ▼ ▼ ServiceNode::start Axum routes - probe storage │ - enroll + renew lease │ - provision both tenant Cells │ + probe storage │ + enroll + renew lease │ + provision both tenant Cells │ │ │ └── scoped handle ◄── authenticate + authorize