diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 26939010..e9aec6dd 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -154,6 +154,27 @@ jobs: pip install "maturin>=1.7,<2" maturin build --release --strip --out dist sccache --show-stats + - name: Verify-FFI fold smoke (CIRISServer#232 — symbols must survive the linker strip) + shell: bash + run: | + python -m pip install --force-reinstall --no-deps dist/*.whl + python - <<'PY' + import ctypes, ciris_server + p = ciris_server.verify_ffi_path() + print("verify_ffi_path ->", p) + lib = ctypes.CDLL(p) + need = [ + "ciris_verify_ffi_link_anchor", "ciris_verify_jcs_canonicalize", + "ciris_verify_self_enc_pubkeys", "ciris_verify_self_enc_respond", + "ciris_verify_wrap_dek_for_recipient", "ciris_verify_unwrap_dek", + "ciris_verify_kex_respond_hybrid_with_public", + "ciris_verify_create_federation_identity", + "ciris_verify_locale_merkle_root", "ciris_verify_admit_attestation", + ] + missing = [s for s in need if not hasattr(lib, s)] + assert not missing, "verify FFI symbols MISSING from _native (linker --gc-sections stripped the fold): %s" % missing + print("OK - verify FFI folded: %d symbols resolvable via ctypes from the built wheel" % len(need)) + PY - uses: actions/upload-artifact@v4 with: name: wheels-${{ matrix.plat.name }} @@ -269,6 +290,27 @@ jobs: # backend), warmed on main by warm-release-cache.yml calling this same # workflow. Restores main-scoped sccache objects on a tag publish. sccache: true + - name: Verify-FFI fold smoke (CIRISServer#232 — symbols must survive the linker strip) + shell: bash + run: | + python -m pip install --force-reinstall --no-deps dist/*.whl + python - <<'PY' + import ctypes, ciris_server + p = ciris_server.verify_ffi_path() + print("verify_ffi_path ->", p) + lib = ctypes.CDLL(p) + need = [ + "ciris_verify_ffi_link_anchor", "ciris_verify_jcs_canonicalize", + "ciris_verify_self_enc_pubkeys", "ciris_verify_self_enc_respond", + "ciris_verify_wrap_dek_for_recipient", "ciris_verify_unwrap_dek", + "ciris_verify_kex_respond_hybrid_with_public", + "ciris_verify_create_federation_identity", + "ciris_verify_locale_merkle_root", "ciris_verify_admit_attestation", + ] + missing = [s for s in need if not hasattr(lib, s)] + assert not missing, "verify FFI symbols MISSING from _native (linker --gc-sections stripped the fold): %s" % missing + print("OK - verify FFI folded: %d symbols resolvable via ctypes from the built wheel" % len(need)) + PY - uses: actions/upload-artifact@v4 with: name: wheels-macos-${{ matrix.target }} @@ -368,6 +410,27 @@ jobs: # Compiler cache (sccache over the GHA backend), warmed on main via # warm-release-cache.yml. Restore-on-tag; harmless otherwise. sccache: true + - name: Verify-FFI fold smoke (CIRISServer#232 — symbols must survive the linker strip) + shell: bash + run: | + python -m pip install --force-reinstall --no-deps dist/*.whl + python - <<'PY' + import ctypes, ciris_server + p = ciris_server.verify_ffi_path() + print("verify_ffi_path ->", p) + lib = ctypes.CDLL(p) + need = [ + "ciris_verify_ffi_link_anchor", "ciris_verify_jcs_canonicalize", + "ciris_verify_self_enc_pubkeys", "ciris_verify_self_enc_respond", + "ciris_verify_wrap_dek_for_recipient", "ciris_verify_unwrap_dek", + "ciris_verify_kex_respond_hybrid_with_public", + "ciris_verify_create_federation_identity", + "ciris_verify_locale_merkle_root", "ciris_verify_admit_attestation", + ] + missing = [s for s in need if not hasattr(lib, s)] + assert not missing, "verify FFI symbols MISSING from _native (linker --gc-sections stripped the fold): %s" % missing + print("OK - verify FFI folded: %d symbols resolvable via ctypes from the built wheel" % len(need)) + PY - uses: actions/upload-artifact@v4 with: name: wheels-windows-x64 diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index b1bc6666..a7ea7b93 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -113,6 +113,36 @@ jobs: maturin build --release --strip --out dist ls -l dist/ sccache --show-stats + # ── Verify-FFI fold smoke (CIRISServer#232) ────────────────────────────── + # The same assertion build-wheels.yml runs on the macOS/Windows legs at + # release time — repeated HERE so it gates every PR/push on linux too, and + # so the smoke script itself is exercised long before it can fail a publish. + # We fold ciris-verify-ffi (rlib) into _native.so, but NOTHING in our Rust + # calls its #[no_mangle] fns (the agent reaches them via ctypes at runtime), + # so --gc-sections would happily dead-strip all ~84 ciris_verify_* symbols — + # per-platform-silently. Load the built wheel exactly the way the agent will + # and assert the surface actually resolves. + - name: Verify-FFI fold smoke (CIRISServer#232 — symbols must survive the linker strip) + shell: bash + run: | + python -m pip install --force-reinstall --no-deps dist/*.whl + python - <<'PY' + import ctypes, ciris_server + p = ciris_server.verify_ffi_path() + print("verify_ffi_path ->", p) + lib = ctypes.CDLL(p) + need = [ + "ciris_verify_ffi_link_anchor", "ciris_verify_jcs_canonicalize", + "ciris_verify_self_enc_pubkeys", "ciris_verify_self_enc_respond", + "ciris_verify_wrap_dek_for_recipient", "ciris_verify_unwrap_dek", + "ciris_verify_kex_respond_hybrid_with_public", + "ciris_verify_create_federation_identity", + "ciris_verify_locale_merkle_root", "ciris_verify_admit_attestation", + ] + missing = [s for s in need if not hasattr(lib, s)] + assert not missing, "verify FFI symbols MISSING from _native (linker --gc-sections stripped the fold): %s" % missing + print("OK - verify FFI folded: %d symbols resolvable via ctypes from the built wheel" % len(need)) + PY - uses: actions/upload-artifact@v4 with: name: ciris_server-wheel-linux-x86_64 diff --git a/Cargo.lock b/Cargo.lock index 6edd63b3..d387280e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,24 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f" +dependencies = [ + "android_log-sys", + "env_logger", + "log", + "once_cell", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -383,6 +401,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -649,6 +678,25 @@ dependencies = [ "cipher", ] +[[package]] +name = "cbindgen" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da6bc11b07529f16944307272d5bd9b22530bc7d05751717c9d416586cedab49" +dependencies = [ + "clap 3.2.25", + "heck 0.4.1", + "indexmap 1.9.3", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", + "tempfile", + "toml 0.5.11", +] + [[package]] name = "cc" version = "1.2.65" @@ -766,8 +814,8 @@ dependencies = [ [[package]] name = "ciris-crypto" -version = "9.0.0" -source = "git+https://github.com/CIRISAI/CIRISVerify?tag=v9.0.0#197105a10f08fe99a575790836bcb54469b266cd" +version = "9.0.2" +source = "git+https://github.com/CIRISAI/CIRISVerify?tag=v9.0.2#946184d4664681cf7d98b8fec54df7a75fee2b63" dependencies = [ "chacha20poly1305", "ed25519-dalek", @@ -793,8 +841,8 @@ dependencies = [ [[package]] name = "ciris-edge" -version = "10.1.0" -source = "git+https://github.com/CIRISAI/CIRISEdge?tag=v10.1.0#32d24c7d5e6cbaa6d556ed100039bfef087d6b9c" +version = "10.1.2" +source = "git+https://github.com/CIRISAI/CIRISEdge?tag=v10.1.2#3da1bd44d18d94b50b4aba9bb8e666d4eb07efb7" dependencies = [ "async-trait", "axum", @@ -847,8 +895,8 @@ dependencies = [ [[package]] name = "ciris-keyring" -version = "9.0.0" -source = "git+https://github.com/CIRISAI/CIRISVerify?tag=v9.0.0#197105a10f08fe99a575790836bcb54469b266cd" +version = "9.0.2" +source = "git+https://github.com/CIRISAI/CIRISVerify?tag=v9.0.2#946184d4664681cf7d98b8fec54df7a75fee2b63" dependencies = [ "aes-gcm", "async-trait", @@ -912,8 +960,8 @@ dependencies = [ [[package]] name = "ciris-persist" -version = "15.1.0" -source = "git+https://github.com/CIRISAI/CIRISPersist?tag=v15.1.0#e1313ccc5d509ecc1ddd61b542de042730346dbd" +version = "15.1.2" +source = "git+https://github.com/CIRISAI/CIRISPersist?tag=v15.1.2#52563d22af97c9fa3c3b2836d420bc89a290cad8" dependencies = [ "async-trait", "base64 0.22.1", @@ -950,7 +998,7 @@ dependencies = [ [[package]] name = "ciris-server" -version = "0.5.105" +version = "0.5.107" dependencies = [ "anyhow", "async-trait", @@ -963,6 +1011,7 @@ dependencies = [ "ciris-lens-core", "ciris-persist", "ciris-verify-core", + "ciris-verify-ffi", "criterion", "ed25519-dalek", "flate2", @@ -992,8 +1041,8 @@ dependencies = [ [[package]] name = "ciris-verify-core" -version = "9.0.0" -source = "git+https://github.com/CIRISAI/CIRISVerify?tag=v9.0.0#197105a10f08fe99a575790836bcb54469b266cd" +version = "9.0.2" +source = "git+https://github.com/CIRISAI/CIRISVerify?tag=v9.0.2#946184d4664681cf7d98b8fec54df7a75fee2b63" dependencies = [ "android_system_properties", "async-trait", @@ -1002,7 +1051,7 @@ dependencies = [ "chrono", "ciris-crypto", "ciris-keyring", - "clap", + "clap 4.6.1", "ed25519-dalek", "futures", "goblin", @@ -1031,6 +1080,56 @@ dependencies = [ "x509-parser", ] +[[package]] +name = "ciris-verify-ffi" +version = "9.0.2" +source = "git+https://github.com/CIRISAI/CIRISVerify?tag=v9.0.2#946184d4664681cf7d98b8fec54df7a75fee2b63" +dependencies = [ + "aes-gcm", + "android_logger", + "base64 0.21.7", + "cbindgen", + "chrono", + "ciris-crypto", + "ciris-keyring", + "ciris-verify-core", + "ctor", + "dirs", + "ed25519-dalek", + "hex", + "hkdf", + "jni", + "libc", + "log", + "oslog", + "rand 0.8.6", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "tokio", + "tracing", + "tracing-log", + "tracing-subscriber", + "ureq", + "webpki-roots 0.26.11", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "strsim 0.10.0", + "termcolor", + "textwrap", +] + [[package]] name = "clap" version = "4.6.1" @@ -1049,8 +1148,8 @@ checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", - "clap_lex", - "strsim", + "clap_lex 1.1.0", + "strsim 0.11.1", ] [[package]] @@ -1059,12 +1158,21 @@ version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.118", ] +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clap_lex" version = "1.1.0" @@ -1207,7 +1315,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap", + "clap 4.6.1", "criterion-plot", "futures", "is-terminal", @@ -1338,6 +1446,16 @@ dependencies = [ "libloading", ] +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.118", +] + [[package]] name = "ctr" version = "0.9.2" @@ -1383,6 +1501,19 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.11.0" @@ -1626,7 +1757,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.118", @@ -1653,6 +1784,16 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "log", + "regex", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -2089,7 +2230,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -2135,6 +2276,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -2217,12 +2364,27 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -2646,6 +2808,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -3790,6 +3962,23 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "oslog" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969" +dependencies = [ + "cc", + "dashmap", + "log", +] + [[package]] name = "outref" version = "0.5.2" @@ -4234,7 +4423,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.118", @@ -4667,7 +4856,7 @@ name = "reticulum-std" version = "0.8.1+ciris.1" source = "git+https://github.com/CIRISAI/leviculum?tag=v0.8.1%2Bciris.1#eb32cf13d696c1f5a28755e61d9f72c4cfbe8c10" dependencies = [ - "clap", + "clap 4.6.1", "hmac 0.12.1", "if-addrs", "libc", @@ -5162,7 +5351,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.14.0", "itoa", "ryu", "serde", @@ -5414,6 +5603,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -5499,6 +5694,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "textwrap" version = "0.16.2" @@ -5758,6 +5962,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.23" @@ -5776,7 +5989,7 @@ version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde_core", "serde_spanned 1.1.1", "toml_datetime 0.7.5+spec-1.1.0", @@ -5809,7 +6022,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.14.0", "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -5820,7 +6033,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -6063,7 +6276,7 @@ dependencies = [ "anyhow", "camino", "cargo_metadata", - "clap", + "clap 4.6.1", "uniffi_bindgen", "uniffi_build", "uniffi_core", @@ -6084,8 +6297,8 @@ dependencies = [ "fs-err 2.11.0", "glob", "goblin", - "heck", - "indexmap", + "heck 0.5.0", + "indexmap 2.14.0", "once_cell", "serde", "tempfile", @@ -6127,7 +6340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4b42137524f4be6400fcaca9d02c1d4ecb6ad917e4013c0b93235526d8396e5" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.14.0", "proc-macro2", "quote", "syn 2.0.118", @@ -6169,8 +6382,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "761ef74f6175e15603d0424cc5f98854c5baccfe7bf4ccb08e5816f9ab8af689" dependencies = [ "anyhow", - "heck", - "indexmap", + "heck 0.5.0", + "indexmap 2.14.0", "tempfile", "uniffi_internal_macros", ] diff --git a/Cargo.toml b/Cargo.toml index 3f88992a..e511e8e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ciris-server" -version = "0.5.106" # 0.5.106 = TRACE DELIVERY FIX (edge v10.0.2->v10.1.0). THE delivery wheel. Minor edge bump; persist v15.1.0 / verify v9.0.0 UNCHANGED; clean pin bump (start() API stable); NO server source change. CIRISEdge#317 FIXED at the source: the announce now carries the signed TRANSPORT x25519, so the receiver admit reconstructs sha256(transport_x25519||transport_ed25519)[..16] = the EXACT identity the RNS link proves. RCA (0.5.104/0.5.105 self-diagnosing wheels): the announce advertised the node FEDERATION identity (AV-42 rooting key) but the RNS link authenticates under the TRANSPORT identity (same ed25519 signing half, different x25519 enc half), so #314 attribution compared two different keypairs -> source_key_id=None -> SkippedNoSourceKeyId -> binary CRPL hit serde_json -> schema_invalid -> Node A never replied -> 0 envelopes. NOT agent config (the announce is edge init_edge_runtime, not the serve_with_python_adapter key_id tag). Also CIRISEdge#318 (bounded peers map). END TO END: source_key_id=Some -> route_inbound_bytes -> #312 responder answers the IdentityOccurrence round -> agent resolve_peer_kex_pubkeys(canonical)=Some -> KEX PRESENT -> seals -> envelopes_sent>0 -> traces land -> CUT 2.9.7. Agent bumps ciris-server>=0.5.106 (one line) + qa_runner safety_battery --federation-delivery. FOLLOW-UP (NOT bundled, to keep this delivery wheel clean): CIRISServer#232 (ship the verify FFI so the agent drops the standalone ciris-verify pin) ships as the next wheel — a per-platform linker-symbol-export change not worth risking on the 2.9.7-critical wheel; the standalone verify at v9.x is fine meanwhile. Full suite 309/309; clippy(default+python)/fmt/gate green. === PRIOR === 0.5.105 = #317 ADMIT DISAMBIGUATOR (edge v10.0.1->v10.0.2). Patch on the 10.x line; persist v15.1.0 / verify v9.0.0 UNCHANGED; clean pin bump; NO server source change. STILL A DIAGNOSTIC WHEEL (not the delivery fix). edge v10.0.2 (CIRISEdge#317 / CIRISServer#235): the conclusive admit-time disambiguator — logs ed25519_halves_match at admit, deciding 2a (federation-identity vs transport-identity SOURCE split) vs 2b (send-vs-announce dest split) in one line. It also surfaced that EDGE LACKS THE TRANSPORT X25519 at admit (it captures the announce/federation identity, which is why the stored hash != the link-proven RNS transport identity hash sha256(x25519||ed25519)[..16] — confirming the RCA refinement: leviculum compute_hash IS combined, so the gap is the identity SOURCE, not the derivation). A rerun on 0.5.105 will still 0-envelope BUT print the verdict -> scopes the aligned-admit fix to one of three shapes (plumb transport x25519 / capture link identity / agent single-identity). THEN edge ships the fix and I adopt it + bundle CIRISServer#232 (verify FFI) in that wheel. Full suite 309/309; clippy(default+python)/fmt/gate green. Open: #317 (fix pending verdict), CIRISEdge#318 (peers-map bound). === PRIOR === 0.5.104 = SELF-DIAGNOSING edge attribution (edge v9.10.1->v10.0.1, #317 observability). Edge major 9->10; persist v15.1.0 / verify v9.0.0 UNCHANGED; clean pin bump (ReplicationRuntime::start API unchanged across v10; the v10.0.0 major is internal per-dimension replication policy realizing CIRISPersist#425). NO server source change. THIS IS A DIAGNOSTIC WHEEL, NOT THE DELIVERY FIX. CIRISEdge#317: on 0.5.103/edge9.10.1 (clean boot) the #314 attribution STILL misses — both branches false (stored transport_identity_hash != link-proven identity_hash AND announced-dest != expected-dest) → source_key_id=None → the gate skips route_inbound_bytes → binary CRPL hits serde_json -> schema_invalid -> Node A never replies -> 0 envelopes. Root cause was AMBIGUOUS (3 candidates: LinkIdentified never fired / announce-identity vs sending-link-identity split / derivation mismatch), so edge v10.0.1 ships SELF-DIAGNOSING observability (my #317 observability spec): link_attribution_miss WARN (throttled, DoS-safe) dumps all four operands + get_remote_identity(remote_identity_present) + the SkippedNoSourceKeyId/NotAReplicationFrame gate decisions + admit-time stored transport_identity_hash/dest. The match LOGIC is UNCHANGED — so a rerun on 0.5.104 will still 0-envelope BUT emit one link_attribution_miss line that pins candidate 1/2/3. THEN edge ships the actual attribution fix and I adopt it (+ bundle #232 verify-FFI per the hold-to-bundle directive). Full suite 309/309; clippy(default+python)/fmt/gate green. === PRIOR === 0.5.103 = TRACE-FLOW COMPLETE (edge v9.10.0->v9.10.1, #314 fix). Edge-only patch; persist v15.1.0 / verify v9.0.0 UNCHANGED; NO server source change. CIRISEdge#314: Node A dropped every inbound CRPL frame from the advisory-admitted agent because the inbound-link->key_id attribution matched by a RECOMPUTED dest-hash FORM (compute_destination_hash(name_hash, link_identity_hash) = named transport dest) against the peer STORED announce dest (*announce.destination_hash()); when those forms differ (named-vs-explicit, the same class as 0.5.100->0.5.101) no match -> source_key_id=None -> the edge.rs:3616 gate SKIPS route_inbound_bytes -> the binary frame falls to verify.verify()->serde_json -> schema_invalid: expected value at line 1 column 1 -> Node A never replies -> agent rounds time out -> resolve_peer_kex_pubkeys=None -> 0 envelopes. FIX (v9.10.1): attribute inbound links by transport IDENTITY (form-agnostic) not the named-dest recompute, so source_key_id populates -> route_inbound_bytes fires -> #312 auto-registers the Responder -> Node A replies with its occurrence -> agent resolves KEX -> seals -> traces flow. Field-diagnosed branch-A (source_key_id absent from the dispatch span, no no-coordinator warn). This closes the trace-flow arc END TO END: the agent (>=0.5.102, CIRISAgent#917) reruns on 0.5.103 and delivers -> cut 2.9.7. check/clippy(default+python)/fmt/gate green, full suite 309/309. === PRIOR === 0.5.102 = TRACE-FLOW UNBLOCK + TRIPLE-MAJOR (edge v9.10.0 / persist v15.1.0 / verify v9.0.0). THE trace-flow closer: edge#312 (advisory-peer RESPONDER auto-register) — Node A now answers the agent inbound anti-entropy rounds it was DROPPING at NoCoordinatorRegistered (it only built Initiator coordinators for consent peers; the agent, admitted-as-advisory, had none). The agent IdentityOccurrence round is now answered with Node A own signed occurrence -> agent resolves Node A KEX pubkeys -> seals -> traces flow. resolve_peer already addressed the advisory peer (present in self.peers w/ dest_hash, field-confirmed), so it was a one-part edge fix, NO server change for the responder. ALSO edge#311 namespace-policy replication engine + persist#425 namespace registry (CC-generated, 95 families/9 components): replication is resolved from a signed envelope namespace/cohort_scope, retiring the per-object selector whack-a-mole. SERVER adopt: ReplicationRuntime::start collapsed key_selector(#257)+occurrence_selector(#305) into ONE self_provider (yields the node key_id; the engine self-publishes Key+IdentityOccurrence+TransportDestination by namespace). ALSO Option A substrate LANDED: verify v9.0.0 (#185) — the accord co-scrub carries infra:attest via ScrubTarget.roles; the build-manifest trust root folds onto the co-scrub, retiring delegates_to. persist v15.0.0 (#422) — check_infra_attest_role_admission gates infra:attest in roles on the m-of-n accord scrub (shared verify_accord_family_coscrub w/ the canonical gate; no self-conferral). SERVER adopt: both ScrubTarget sites (admit-node + propose_canonical) carry roles: vec![] (empty = today; the future ci-key co-scrub sets ["infra:attest"]). This UNBLOCKS wiring the Trust Root trust_ci_worker card + /v1/accord/ci-key/{propose,cosign} (next). No source behavior change beyond the two adopt-fixes; full suite 309/309, clippy(default+python)/fmt/gate green. 0.5.101 named-dest recompute retained (field-proven; edge#309 local_named_dest_hash cleanup deferred). === PRIOR === 0.5.101 = SELF-OCCURRENCE NAMED-DEST FIX (unblocks inbound sealing on 0.5.100 nodes). NO substrate change (persist v14.1.0 / verify v8.12.0 / edge v9.8.0 held); server-only. RCA: 0.5.100 publish_self_identity_occurrence put edge.local_dest_hash() — the EXPLICIT hash sha256(fed_pubkey)[..16] (v7.0.0 direct-dial) — into the occurrence transport_destination, but verify_signed_identity_occurrence recomputes the NAMED hash sha256(name_hash("ciris"."edge") || sha256(x25519||ed25519)[..16])[..16] per §5.6.8.8.1.1 → DestinationHashMismatch → self-publish rejected → peers resolve None → inbound sealing blocked (field-reported on Node A). The 0.5.100 e2e BUILT the envelope with verify compute_destination_hash (the named formula) so it matched the gate by construction and never exercised the local_dest_hash() call — the bug lived only in prod. FIX: compose computes destination_hash with the gate own compute_destination_hash("ciris",["edge"],x25519,ed25519) — byte-identical to edge local_named_dest_hash (NAME_HASH_LEN=10/DEST_HASH_LEN=16, identity_hash=sha256(x25519||ed25519)[..16], x25519@[0..32]) — so the occurrence carries the named dest edge announces+listens on for mesh delivery, gate accepts, peers seal. Follow-up filed: expose Edge::local_named_dest_hash so a future release uses edge authoritative value instead of recomputing (drift-proof). === PRIOR === 0.5.100 = SIGNED OCCURRENCE-KEX (the arc 4/4 close-out: CIRISVerify#183 + CIRISPersist#418 + CIRISEdge#305/#307 adopted; CIRISServer#227 S1+S3). Substrate triple persist v13.9.1->v14.1.0 (MAJOR) / verify v8.10.1->v8.12.0 / edge v9.7.0->v9.8.0. THE GAP (user-called): content-enc was never a CUSTODY capability and the occurrence rode the wire UNSIGNED. Closed end to end: (1) verify v8.12.0 = SelfEncKeys (keyring: enc_pubkeys + kex_respond INSIDE the seal — retrieve->HKDF->scrub, no private half ever crosses an API; deterministic so restore re-derives identical keys) + produce_signed_identity_occurrence (the producer byte-matching the long-existing verify_transport_binding verifier) + by-alias FFI. (2) persist v14.0/14.1 = SignedIdentityOccurrence carries {attesting_key_id, signed_envelope, signature}; put_identity_occurrence is ONE fail-secure gate (hybrid sig over JCS envelope, dest-hash recompute §5.6.8.8.1.1, C4 transport/content-KEM separation, signer_acts_for) + LAST-SIGNED-WINS upsert (anti-first-writer poison) + put_identity_occurrence_local (trusted-local content-only device binds, NULL sig columns, EXCLUDED from replication) + list_signed_identity_occurrences_for (v14.1.0: byte-exact signed re-read — a replicator cannot re-sign, it re-wraps the signed tuple verbatim and the receiver re-verifies the SAME signature). (3) edge v9.8.0 = #305 rewired to publish from the signed re-read. SERVER: (a) compose::publish_self_identity_occurrence — boot self-publish of THIS node's SIGNED occurrence, the sealability twin of publish_self_transport_destination (transport binding = how to REACH me; occurrence = how to SEAL to me): enc pubkeys from SelfEncKeys (sealed custody, hw/sw does NOT matter), envelope carries the REQUIRED transport_destination (edge transport identity, app "ciris"/aspects ["edge"], gate recomputes the dest hash) + encryption_pubkeys, signed by the node's own hybrid signer (attesting == identity's own key). Idempotent per boot (fresh asserted_at supersedes). THE AGENT DOES NOTHING — the node self-publishes its sealability; the agent's only remaining op is the by-alias custody respond. (b) bind_occurrence_core -> put_identity_occurrence_local (content-only DEK-cascade binds; never signed-replicate). (c) tests/occurrence_kex_e2e.rs REWRITTEN to the test it should have been (the QA lesson: the 0.5.99 version fixtured the raw-seed + unsigned assumptions and passed by construction): sealed-custody fixture (SelfEncKeys by alias; raw seed touched ONLY at mint-time adopt), forged occurrence (registered-but-unrelated signer claiming another identity) REJECTED, tampered envelope REJECTED, byte-exact signed replication re-verified at the receiver, trusted-local rows excluded from the wire, rotation last-signed-wins + stale-replay no-op, seal round-trip with kex_respond INSIDE custody. HELD BACK deliberately: IdentityOccurrenceRevocation carriage (#227 S2) — the revocation half is STILL UNSIGNED in persist v14.1.0 ({identity_occurrence_revocation}, no sig container); an unauthenticated wire revocation = any consented peer can kill any identity's sealability (DoS). Flagged on CIRISPersist#418; wire the kind only when signed. === PRIOR === 0.5.99 = OCCURRENCE-KEX PLANE (CIRISEdge#305 adoption) — the last trace-flow domino: a rooted peer can finally be SEALED to. Edge bump v9.6.1->v9.7.0 (persist v13.9.1 / verify v8.10.1 floor UNCHANGED). RCA: content sealing needs the peer occurrence encryption_pubkeys (x25519+ML-KEM) that resolve_peer_kex_pubkeys reads from federation_identity_occurrences — a DIFFERENT directory object + replication plane (EnvelopeKind::IdentityOccurrence) than the transport binding 0.5.98 roots. Edge#305 (v9.7.0) added the publish-own occurrence_selector (KEX analogue of the #257 key_selector): list_identity_occurrences now honors it instead of only fanning out over the cohort, so a node advertises its OWN occurrence (enc keys). SERVER companion: (1) build_replication_peers + replication_reconcile add the EnvelopeKind::IdentityOccurrence coordinator per peer (the plane was never exchanged — only Attestation+Key) so anti-entropy carries occurrences both ways; (2) start_replication_runtime passes an occurrence_selector yielding the node key_id (publish-own). DIVISION (agent=node=one keypair): the AGENT derives the node content-enc keypair from the node Ed25519 seed at mint, publishes its self-occurrence via put_identity_occurrence, and uses the private halves in federation_session_respond; the SERVER only replicates (never holds content-enc keys / never seals). Once both ends run this, the agent resolve_peer_kex_pubkeys(canonical) flips None->Some, the trace seals, envelopes_sent>0. Arc: rootable(#397)->auto-dial(#296/#404)->survives-restart(#411/#299)->first-rootable(#301/#413)->agent-fold(#221)->advisory-durable(#416)->canonical-prime(0.5.98)->occurrence-KEX(#305). === PRIOR === 0.5.98 = CEG-NATIVE TRUSTED-PEER BOOT PRIME (CIRISServer#221 companion) — roots the explicit-hash canonical for OUTBOUND delivery. NO substrate change (edge v9.6.1 / persist v13.9.1 / verify v8.10.1 held); server-only. RCA: the explicit-hash canonical (ciris-canonical-1, v7.0.0, Leviculum ExplicitHashCannotAnnounce) is the OUTBOUND target the node dials at :4242 — it never self-roots via the inbound admit-advisory path (#301), so it must be rooted out-of-band via prime_peer. start_federation_delivery primed it on the embedded edge, but compose::serve_with_adapter (the seam the agent fold #221 runs) never did → in the folded node knows_peer(canonical)=false → anti_entropy_round coordinator error → envelopes_sent=0 (field-reported). FIX: compose now runs prime_trusted_peers on boot — CEG-native, NO hardcoded canonical: it primes EVERY transport_destinations row with BindingProvenance::Rooted (authoritative — federation-key-signed occurrence / root_binding-verified / self-published/replicated). Advisory rows (#301, routing hints) are skipped; an operator who untrusts a canonical/community withdraws the row → not primed → trust is pure CEG state load. On first boot the Rooted set IS the baked canonicals (their only rooting path, since they can't announce). Mirrors start_federation_delivery's prime but driven by the full directory (list_all_transport_destinations, #411), so the node-composed runtime — the fold — roots its trusted peers. Best-effort (missing/undecodable binding or transport-less build warns+skips). Runs in both the standalone node and the fold (shared edge). NOW: fold boot → prime the Rooted canonical → knows_peer=true → KEX → envelopes ship. 0.5.97 = ADVISORY-BINDING PERSISTENCE (CIRISPersist#416) — closes the last durable gap in the trace-flow arc. Substrate bump persist v13.9.0→v13.9.1 / edge v9.6.0→v9.6.1 (verify v8.10.1 unchanged). PURE substrate adoption, NO server source change. CIRISPersist#416 (v13.9.1, migration V101): drops the transport_destinations FK to federation_keys. RCA: V078 declared occurrence_key_id NOT NULL REFERENCES federation_keys(key_id) ("no address for an unknown key"), but V100/#413's admit-advisory model (CC 3.3.6.2) RECORDS a self-consistent announce whose key is NOT in federation_keys (authority not established) — so the FK rejected exactly the advisory row it was built to record. Field-proven in the 0.5.96 agent fold (CIRISServer#221): Node B (ciris-status-1) rooted as Advisory via edge#301 → admitted in-memory but `put_transport_destination: FOREIGN KEY constraint failed / provenance=Advisory` → binding was session-only (died on restart, blocked resolve_peer_kex_pubkeys → 0 envelopes). V101 recreates the table without the FK; the binding_provenance tag + routing-time preference (prefer Rooted; content gates on trust, CC 6 N1) are the replacement correctness. Rooted writes unaffected (their keys are in federation_keys regardless). edge v9.6.1 = coherence re-pin of persist v13.9.1. NOW: an advisory cold-start-rooted peer admits + KEXes + DURABLY PERSISTS + survives restart → boot-loads (#411/#299) → resolve_peer_kex_pubkeys resolves the x25519 → KEX → envelopes ship. THE TRACE-FLOW ARC IS COMPLETE END TO END: rootable (#397) → auto-dial (#296 + :4242 #404) → survives-restart (#411/#299) → first-rootable (#301/#413) → agent-fold (#221) → advisory-durable (#416). 0.5.96 = AGENT-FOLD SEAM (CIRISServer#221) — the last domino for trace-flow. NO substrate change (edge v9.6.0 / persist v13.9.0 / verify v8.10.1 held). serve_with_adapter (the seam `serve_with_python_adapter` folds the Python brain onto for the federation/self/accord routes on 4243) now REUSES the agent's in-process embedded engine + edge instead of building fresh: (1) build_engine → if current_rust_engine() is Some (the brain installed the persist process-singleton via Engine(config)), reuse it — the prior with_hardware_signer_hybrid opened a SECOND connection pool + sweeper on the same SQLite DSN (WAL-safe but SQLITE_BUSY-prone under a bursty cognitive loop); (2) build_edge → if embedded, reuse current_edge() (the running Arc from init_edge_runtime) instead of binding a SECOND Reticulum transport on :4242 (a hard address-in-use conflict with the agent's transport); (3) the edge.run() spawn is SKIPPED when embedded (the agent already owns the run loop). Gated on the `python` feature + current_rust_engine()==Some, so the standalone binary (serve → NoopAdapter) is byte-for-byte unchanged (embedded=false → build fresh). Slices attach + read-API mount unchanged (&Arc deref-coerces to &Edge; routing registries edge reads live per inbound frame). This lets CIRISAgent's BrainAdapter + main.py → serve_with_python_adapter fold in cleanly and additively — /v1/federation/announce on 4243 → announce → root → KEX → envelopes. CI: bundles PR #218 (sccache wired into the linux wheel legs — they were cold, no compiler cache; now ~86% warm like windows, ~15-20 min compute saved). NB caveat (boot-test verifies): several slices are documented "MUST run BEFORE edge.run()"; in the fold they run after the agent's run() — replication routing is confirmed live-OnceLock-safe, the control responder + holonomic are boot-test-to-confirm. 0.5.95 = FIRST-ROOTABLE (admit-as-advisory) — the mesh roots peers end to end. Substrate bump persist v13.8.0→v13.9.0 / edge v9.5.0→v9.6.0 (verify v8.10.1 unchanged). CIRISEdge#301 + CIRISPersist#413, grounded in CIRISConstitution CC 3.3.6.2 (an unauthenticated announce is advisory-only — a routing hint the substrate ADMITS + RECORDS + KEXes, never drops; trust is consumer-composed, not a substrate verdict) + CC 5.3.3.5 E1 (KEX = hop-by-hop path-confidentiality, a separate layer UNDER the E2E content DEK) + CC 6 N1 (trust≠membership; admission gated at the destination on CONTENT, never on the connection). THE FIX: edge's announce handler stops dropping unauthenticated announces — root_binding now CLASSIFIES (Rooted | Advisory) instead of gating. An unknown/unrooted peer is admitted as Advisory (self-signature-verified routing hint), recorded, and KEX'd — so a FRESH peer can finally first-root (the CIRISServer#216 wall). advisory→rooted upgrade on the same-epoch re-announce that roots; forged self-claims still dropped (the AV-42 dest_hash crypto floor is preserved). persist v13.9.0: TransportDestination gains binding_provenance: Rooted|Advisory (default Rooted, back-compat) so the tag is durable + boot-loaded (#411/#299); competing claims on a destination are admitted + surfaced, resolved by preference at routing time (prefer Rooted), never a substrate reject. RCA correction baked in: the prior owner-gated-admission behavior read the code's buggy drop-on-unknown as intent — the constitution says a server does NOT opt in to receiving. SERVER wiring: the 4 authoritative TransportDestination construction sites (self-publish, accord canonical address, occurrence-bind, prime-resolve fixture) tag binding_provenance: Rooted. NO other server source change — content-acceptance already gates on verify-against-directory (CC 6 N1), which composes correctly with edge admitting the advisory link. Arc COMPLETE: rootable (#397) → auto-dial (#296 + :4242 #404) → survives-restart (#411/#299) → first-rootable (#301/#413). Follow-on centipede (post-0.6, stronger control — trust-root-blessed build attestation before durable save, software-node-compatible): CIRISVerify#181 → CIRISPersist#415 → CIRISEdge#303 → CIRISServer#219. 0.5.94 = SURVIVES-RESTART + BOOT-UNBRICK (completes the trace-flow substrate arc). Substrate bump persist v13.6.1→v13.8.0 / edge v9.4.1→v9.5.0 (verify v8.10.1 unchanged). Carries the whole line end to end: (1) CIRISPersist#410 (v13.7.0) — the genesis canonical-seed re-bake no longer BRICKS boot on a node that already holds a different anchor-scrubbed record (the :4243→:4242 correction bricked Node A on 0.5.93; seed_canonical_servers now tolerates/upgrades instead of Err-aborting) → NODE A + the whole 0.5.86+ fleet can finally upgrade off 0.5.92. (2) CIRISPersist#411 + CIRISEdge#299 (v13.8.0 / v9.5.0) — SURVIVES-RESTART: the rooted-peer transport binding is now DURABLE. persist's TransportDestination gains transport_x25519_pubkey_base64 (the KEX half, pair to #397's ed25519) + list_all_transport_destinations; edge write-throughs each rooted binding (dest-hash + ed25519 + x25519) to persist on announce-root and BOOT-LOADS all bindings into the transport resolver at startup → a known peer is knows_peer=true and SEALABLE with zero announces after a restart. persist is the source of truth; boot reloads valid rooted state from persist — no re-announce, no re-peer. This is the direct fix for CIRISServer#216 (empty AV-42 registry → resolve_peer_kex_pubkeys=None → 0 envelopes sealed). SERVER wiring: publish_self_transport_destination now saves the X25519 half too (transport_pubkey[0..32]) so THIS node's own binding is fully sealable; the accord canonical address-update (CanonicalAddressUpdate) carries transport_x25519_pubkey_base64 (skip_serializing_if — prior threshold sigs stay byte-identical) so an address move preserves sealability; occurrence-bind + prime-resolve carry the new field. (3) CIRISPersist#405 (overlay honored) rides v13.8.0. NO other server source change. The trace-flow arc: rootable (#397) → auto-dial (#296 + :4242 #404) → address-move (#405/#410) → survives-restart (#299). 0.5.93 = CANONICAL PORT FIX + m-of-n REPLACE (completes the zero-delivery fix end to end). Substrate bump persist v13.6.0→v13.6.1 / edge v9.4.0→v9.4.1 (verify v8.10.1 unchanged). (1) CIRISPersist#404 (v13.6.1): the baked canonical genesis seed (src/federation/genesis/canonical_seed.json) carried the READ-API port in the signed transport hint — `108.61.242.236:4243` (listen+1, the HTTP port) instead of the Reticulum transport port `:4242`. Every fresh node (incl. the agent) auto-seeded its dial (CIRISEdge#296) from that hint → dialed :4243 → spoke RNS at the HTTP API → never rooted. RCA: the read-API port was supplied at the co-scrub ceremony (the client base URL defaults to :4243, so it's the port a human copies) and sealed into the scrub-signed registration_envelope. FIX = re-ran the 2-of-3 co-scrub (A1+B1) over the SAME identity (ciris-canonical-1-d7bdeu223k, pubkeys unchanged) with destination=:4242, persist re-baked the seed. A fresh install now dials :4242 and roots the canonical zero-touch. edge v9.4.1 = coherence re-pin of persist v13.6.1. (2) CLIENT m-of-n replace fix: the Trust Root "Supersede/replace" of an existing canonical routed to the LEGACY 1-of-N addCanonicalServer (CIRISServer#174 migrated Add→co-scrub but left replace behind) — a 1-scrub record FAILS the canonical admission gate (needs distinct_scrub_count ≥ family quorum 2) on fresh installs. Both add AND replace now go through the m-of-n co-scrub (proposeCanonical); the 1-of-N /v1/accord/canonical/add path is now unused from the UI (retire separately). NO server source change (pure substrate adoption + client). Filed CIRISPersist#405 (canonical_bootstrap_hints should honor the mutable transport_destination overlay so a future address move takes effect at runtime without a re-bake). 0.5.92 = TRACE-FLOW UNBLOCK (the release that gets federation traces flowing). Substrate bump persist v13.5.0→v13.6.0 / edge v9.3.0→v9.4.0 (verify v8.10.1 unchanged). CIRISEdge#296: init_edge_runtime now AUTO-SEEDS the canonical TCP dial from persist's baked canonical_bootstrap_hints(). RCA (3-day chase): edge seeded its dial ONLY from the caller-passed bootstrap_peers (pyo3.rs:4741) — rooting was wired + the canonical identity key baked, but the agent-embedded edge (which does NOT run compose::serve) booted with an EMPTY canonical dial → no link to the public canonical → never received its announce → the (already-wired) rooting never fired → knows_peer(canonical)=false → zero delivery. Neither the bake nor edge-init nor rooting was broken; the ONE missing piece was the "read baked hints → seed dial" glue that lived only in ciris-server's compose::serve. Now init_edge_runtime calls the engine's #402 canonical_bootstrap_hints() pyfn cross-wheel, filters kind==ip, unions the TCP dials in (dedup, fail-soft, fully logged) — EVERY direct consumer (the agent) now dials + roots the canonical mesh with zero caller glue, exactly what compose::serve does. CIRISPersist#402 (v13.6.0): canonical_bootstrap_hints() exposed to Python (PyEngine → JSON [{key_id,kind,destination}]) so edge reads a clean hint list instead of parsing signed envelopes. NO server source change — pure substrate adoption; compose::serve's explicit canonical_bootstrap_addrs union is now redundant-but-harmless (dedup covers it), retire next release. ALSO bundles the CI windows-sccache warm (PR #211: warm-release-cache.yml restored, substrate-bump-gated — v0.5.91 windows was 100% cold, proven 0% sccache hits / 445 misses / 21min, because nothing warmed the maturin sccache on main). 0.5.91 = ZERO-TOUCH DELIVERY + CI TRIM (no substrate change; triple stays persist v13.5.0 / edge v9.3.0 / verify v8.10.1). (1) BOOT SELF-PUBLISH (#205 gap #2, the durable close): compose now publishes THIS node's reticulum transport-tier binding (dest_hash from edge.local_dest_hash() + the transport Ed25519 from edge.local_transport_pubkey()[32..64]) into the federation directory at boot, for its OWN key_id — so a v7.0.0 explicit-hash canonical (which cannot announce) is primeable straight from the directory with ZERO operator action: restarting Node A on 0.5.91 IS the publish (the row replicates → a consuming node's start_federation_delivery resolves + prime_peer-roots it → knows_peer flips → delivery converges). Best-effort, self-authenticating (own key_id + own edge-owned transport identity, same basis as an attested announce), no accord quorum needed to declare own reachability; silent no-op on a non-reticulum node. (2) CI-REDUNDANCY TRIM: ci.yml + localization.yml are now pull_request-only — a squash-merge is byte-identical to the PR that just passed, and neither holds a CIRISCache save (that lives in conformance.yml, still on main), so the main-push re-run was pure duplication. conformance's main run (save) + the tag's publish conformance-gate (validates the actual published wheel) are kept — not pure dups. 0.5.90 = TRANSPORT-PRIME (delivery close-out). Substrate triple persist v13.4.2→v13.5.0 / edge v9.2.0→v9.3.0 / verify v8.10.0→v8.10.1. (1) rust-toolchain.toml EXACT-pin stable→1.97.0 (CIRISEdge#291) — `stable` floats, so the CIRISCache key `-rustc-` never matched the substrate repos exact pin → windows/linux wheel restored COLD; aligning all four repos on 1.97.0 makes the restore hit the substrate layer. (2) persist v13.5.0 #397: TransportDestination gains transport_ed25519_pubkey_base64 (Option) — the transport-tier Ed25519 that pairs with a reticulum dest-hash so a peer can prime_peer an explicit-hash canonical (which cannot announce). The accord canonical-address-update invocation (CanonicalAddressUpdate) now carries it (skip_serializing_if keeps pre-#397 threshold sigs byte-identical); the generic occurrence-bind leaves it None. (3) start_federation_delivery now prime_peers the admitted explicit-hash canonicals from the resolved directory binding (gap #1 of #205) + edge v9.3.0 rooting observability (#292). (4) 1.97 clippy: lens-core manual Option::filter → .filter(). 0.5.89 = FEDERATION DELIVERY (#205, subsumes #204). edge v9.1.7→v9.2.0 (CIRISEdge#289: current_edge() downstream-public + require_local_signer attested-announce; persist v13.4.2 / verify v8.10.0 unchanged) + `ffi-uniffi` on the edge dep so ciris_edge::current_edge() links into the controller. NEW pyo3 entry `start_federation_delivery(cadence_seconds=None, announce_logger=True) -> int` (src/federation_delivery.rs + src/lib.rs): the bare AGENT edge, after init_edge_runtime, calls ONE thing that runs the compose delivery controller in-process against current_rust_engine()+current_edge() — reads the baked canonical transport_hints→targets (subsumes #204's read), authors this node's directed consent:replication grant per admitted canonical peer, starts the ONE ReplicationRuntime seeding the canonical key_ids, installs inbound routing (safe post-boot — Edge::run reads the routing OnceLock live per frame), spawns the consent reconcile loop (set_peers) + announce logger. Compose boot UNCHANGED — factored setup_peer_replication's core into compose::start_replication_runtime(engine,edge,node_key_id,extra_targets) (compose passes &[], the controller passes canonical key_ids) + a shared build_replication_peers. CONTRACT: init_edge_runtime(engine=from_shared_with_local(...), disable_reticulum=False, require_local_signer=True, announce_interval_seconds=15) → start_federation_delivery(). CAVEAT: the Reticulum transport only adds a TCP *dial* peer at BUILD time (no runtime add-peer), so the canonical IP MUST be in the edge init bootstrap_peers (agent-side, the user's peers=1 path) — the controller drives everything downstream of the dial. Live delivery is the agent live-lens QA (not unit-testable: needs a live edge + Node A); 4 new unit tests cover peer-set assembly / dedup / empty sets / uninitialized-guard. 0.5.88 = NODE-A BOOT FIX (triple adoption). Substrate bump persist v13.4.1→v13.4.2 / edge v9.1.6→v9.1.7 / verify v8.9.0→v8.10.0. CIRISPersist#394: the #390 canonical bake made seed_canonical_servers `put_public_key` the baked 2-of-3 record — but on the CANONICAL NODE ITSELF (ciris-canonical-1-d7bdeu223k already exists as its own self-signed `node` row, same pubkeys, minted at first registration), put_public_key saw same-key/different-content → Err → EngineError::GenesisSeed → BOOT ABORT. The one node the bake was meant to serve couldn't boot on v13.4.0/v13.4.1 (RCA'd here; bisected 0.5.85✅/0.5.86❌). v13.4.2 seeds via adopt_scrub_upgrade instead: absent→insert, self-signed same-pubkey→UPGRADE to the scrubbed canonical record (A self-roots), already-scrubbed→no-op, drift→reject. verify v8.9.0→v8.10.0 (persist v13.4.2 requires it): additive — the new `ciris-verify manifest sign` CLI (#176/#177, the canonical build-manifest producer emitting SignedCegObject/build_manifest_contribution scoped infra:attest); no server consumer break. edge v9.1.7 = coherence re-pin of persist v13.4.2 + verify v8.10.0. NO server source change — pure substrate adoption; unblocks Node A's upgrade off 0.5.85. 0.5.87 = PYO3-SEED FIX ADOPTION. Substrate bump persist v13.4.0→v13.4.1 / edge v9.1.5→v9.1.6 (verify v8.9.0 unchanged). CIRISPersist#392: the pyo3/wheel ctor PyEngine::new hand-rolled its own genesis seed and stopped at the accord holders — it never ran the #386 entrenched-family (v13.3.0) or #390 canonical (v13.4.0) bakes that Engine::with_signer does. So EVERY wheel consumer (the server's own py_main boot + agent-embedded engines, CIRISServer#191 / CIRISAgent#896) got A1/B1/C1 but NO HUMANITY_ACCORD family row and list_canonical_servers()==[] — the canonical bake never reached the installs it was meant to light up. v13.4.1 factors the full post-holders sequence (verify anchor → family #386 → canonical #390) into ONE shared genesis::seed_family_and_canonical(dir) called by BOTH ctors, so pyo3 + Rust engines are seed-identical. edge v9.1.6 = coherence re-pin of persist v13.4.1. NO server source change — pure substrate adoption; a fresh wheel Engine now returns ciris-canonical-1 + an entrenched family. 0.5.86 = GENESIS-BAKE + GRAPH-FIX. Substrate bump persist v13.3.1→v13.4.0 / edge v9.1.4→v9.1.5 (verify v8.9.0 unchanged). (1) CIRISPersist#390/#391: the 2-of-3 canonical genesis server (ciris-canonical-1-d7bdeu223k) is now BAKED at genesis — the operator's live accord-co-scrubbed record (A1 primary + B1 in additional_scrubs, field-conferred this session) restores the seed #383 deferred (a 1-of-N founding record was a first-strike weakness; canonical ADD is 2-of-3, so the baked genesis is too). A FRESH install boots already trusting ciris-canonical-1 (adopt_scrub_upgrade seeds/upgrades every node's row incl. Node A's own → A self-roots on upgrade), nodes addressable by key_id — no ceremony. edge v9.1.5 = coherence re-pin of persist v13.4.0 (same-repo [patch] gotcha). (2) GRAPH-VIEW FIX: the memory graph crashed loading (JsonConvertException: NodeType has no 'attestation') — 0.5.85's seed_ceg_graph projects CEG trust-root kinds (owner/owned_node/canonical_server/family/holder/delegation/attestation/peer) the client's strict NodeType enum didn't know, so ONE unknown value failed the whole /v1/memory/timeline payload. Fix (client): added the 8 CEG variants + a TOLERANT NodeTypeSerializer (unknown→UNKNOWN, never throws again) + distinct CEG-plane colors. 0.5.85 = SEED-UX cut (no substrate change; edge v9.1.4 / persist v13.3.1 / verify v8.9.0). The fixes that let the operator + a second holder seed the canonical mesh cleanly from the desktop UI: (1) OS-AWARE ykcs11 module path — the node's default PKCS#11 module now resolves per-OS (macOS Homebrew .dylib Apple-Silicon/Intel; Windows Yubico PIV Tool .dll / PATH; Linux .so) via identity.rs::default_ykcs11_module, wired into Pkcs11Options + the accord propose/cosign/admit paths, so a macOS/Windows holder's YubiKey cosign works (was hardcoded Linux → dlopen failure). Optional per-request "PKCS#11 module path (advanced)" field in the hardware-scrub sheet as an override. (2) COPY/EXPORT the co-scrub partial + finished record from the Trust Root card (Propose/Cosign/Pending) — Copy-to-clipboard + Save-to-file; no more pulling JSON off disk. (3) NAV label "Accord" → "Trust Root". (4) CIRISServer#127++ — the memory graph now PROJECTS persist's CEG state (seed_ceg_graph): ~40+ nodes (identity, owner, owned nodes, HUMANITY_ACCORD family + holders, canonical servers, config:*, delegations/consent/structural attestations) with CEG-native typed edges (delegates_to owner-binding, has_member family→holder seat, scrub_conferral holder→canonical, has_config, replicates_to, authored, supersedes/withdraws/recants); each node carries kind·subject·status·record so the client can render it as an AttestationCard. Follow-ups: wire the Trust Root ⋮ hamburger onto graph nodes (client); 28-language fan-out for the 7 new strings; pending-co-scrub graph node. 0.5.84 = THE FULL SAFE-MESH-SEED CUT. Substrate lockstep bumped edge v9.1.2→v9.1.4 / persist v13.2.0→v13.3.1 (v13.3.1 #387 adds the test-only with_signer_no_genesis_seed seam so the accord ceremony tests get a clean engine) (verify v8.9.0 unchanged). (1) CIRISPersist#386: the HUMANITY_ACCORD federation_families row (quorum:2/3, A1/B1/C1) is now SEEDED at boot on every node (idempotent) — lookup_family resolves durably, so the 0.5.83 baked-genesis fallbacks are RETIRED (recognized_family_from_baked_genesis + family_quorum_m fallback deleted; get_family reads the real entrenched row; V097 drops the family_key_id→federation_keys FK — a constitutional family is keyless). (2) CIRISServer#181 producer hook: POST /v1/safety/flag (duty-gated: verify_request + admit_moderation_action(Moderate)) emits the substrate-reserved content_class:{infohazard|reported} flag via a NEW node-scoped substrate_persist identity (sealed Ed25519 -substrate + software ML-DSA seed, registered through register_federation_key) — the duty-holder authorizes, the substrate signs; action:clear supersedes. This makes the #161 infohazard reveal gate actually FIRE (flag → GET /v1/safety/reveal 403 interstitial for a non-consented viewer). (3) CIRISServer#161 (already on main): POST /v1/safety/reveal — the CC 4.5.13 consent gate. #378 single-owner marker gate rides the persist bump (no server action). Follow-ups: retire ensure_accord_family_anchor throwaway key (low-risk, legacy assemble path); infohazard consent lifecycle + interstitial UX (#180 umbrella / #182-#185). 0.5.83 = BAKED-FAMILY RECOGNITION + RAISE-A-HALT. Same substrate (edge v9.1.2 / persist v13.2.0 / verify v8.9.0). Fixes "No accord family established on this node yet" on a node that has the BAKED genesis but no entrenched federation_families row (persist seeds the 3 holder KEY rows, not the family row — durable fix filed CIRISPersist#386): GET /v1/accord/family + the co-scrub quorum_needed now fall back to verify's baked humanity_accord_genesis() (the 2/3 family + 3 seats, entrenched=false, recognized_via=baked-genesis) — the same recognition resolve_kill_switch_roster already uses. PLUS raise-a-halt: POST /v1/accord/halt (initiate_halt, the binding twin of /drill — one opener signature is sub-quorum and cannot latch; 2-of-3 concur latches) + the client "Halt the mesh" action (was a disabled placeholder). 0.5.82 = CO-SCRUB END-TO-END. Same substrate lockstep edge v9.1.2 / persist v13.2.0 / verify v8.9.0 (no triple change). The cross-device m-of-n seed now works from the app: the co-scrub partial GOSSIPS over the accord peer-plane (the same HTTP set the kill-switch uses) — propose on A1's box → the partial surfaces under "Pending co-signs (canonical)" on B1's box → cosign → conferred, no manual transfer. Server (src/accord_provision.rs): ProvisionState gains an ephemeral bounded pending store + a (target,scrub-count) seen-set; ingest_partial validates structurally + upserts + gossips; NEW open (non-loopback) POST /v1/accord/canonical/gossip-partial peer-receive + GET /v1/accord/canonical/pending; router() split into build()→{loopback,gossip}. Client (#174): proposeCanonical/cosignCanonical/listPendingCoscrubs, the "Pending co-signs (canonical)" section + CanonicalCosignSheet (pending-entry or pasted-partial fallback), "[+ New] Add"→"Propose a canonical server". Bundled: #117 (Delegations loads on screen entry, not VM init → no pre-login 401) + #134 (existing-key offer no longer gated on a typed label; label auto-populates from the picked key_id). 0.5.81 = M-OF-N TRUST ROOT. Substrate lockstep edge v9.1.2 / persist v13.2.0 / verify v8.9.0 (CIRISPersist#383: KeyRecord.additional_scrubs + the DYNAMIC m-of-n admission gate via verify's verify_quorum_policy — canonical is conferred iff distinct_scrub_count() ≥ the family's entrenched quorum:M/N, NOT a hardcoded 2; the 1-of-N genesis is no longer auto-baked). Trust Root co-scrub ceremony (server-side, hardware-signed via the shared open_holder_identity custody path): POST /v1/accord/canonical/{propose,cosign} (scrub #1 → gossip/transfer the partial → append_scrub over the byte-identical envelope → adopt_scrub_upgrade lets the m-of-n gate confer/hold), plus /withdraw + /supersede (#377, 2-of-3 via proposal_digest) and IP-in-envelope TransportHint. Accord ops drill + announce (/v1/accord/{drill,announce}, node-synth'd invocation, gossiped, recorded pre-latch) + /halt-status. DRY audit landed (recipes routed through the substrate: build_self_key_record→produce_self_key_record, CANONICAL_BOOTSTRAP_PEERS→Engine::canonical_bootstrap_hints, owner_binding consts aliased). CEG-native Trust Root card redesign (one AttestationCard + ⋮ hamburger; concur now hardware-signs). KNOWN → 0.5.82 (#174): the client "Add canonical" still posts /add (1-scrub partial under m-of-n) — wire the propose→cosign buttons. 0.5.80 = THE MESH-SEED RELEASE. Substrate lockstep edge v9.1.0 / persist v13.0.1 / verify v8.7.0 (CC 1.0 RC1: KeyRecord.consent_role, the accord-conferred `canonical` identity_type role + admission gate CIRISPersist#372, single-owner owner_of #162, CIRISEdge#277 upgrade-aware Key-plane apply → KERI publish-own end-to-end). Trust Root canonical ops (#164): POST /v1/accord/canonical/add — the 1-of-N hardware-scrub SEED op (scrub-sign w/ the canonical role + adopt onto own row + publish address) + GET /v1/accord/canonical/servers + the client "Canonical servers" card (update-address shipped #165; supersede/withdraw → 0.5.81, need a persist role-removal primitive). ci.yml revived (my #155 evidence-step colon had broken the Rust gate repo-wide). 0.5.79 = substrate triple edge v8.7.2 / persist v12.5.0 / verify v8.7.0 (cross-cdylib block_on-deadlock/SIGABRT fix + CC 0.9.3 gates + dep-trims landed: hickory 0.25 de-dup, dead openssl/qrcodegen/bip39 gone) + REMOTE adopt-scrubbed endpoint (#150 — POST /v1/federation/adopt-scrubbed applies an A1-scrubbed record onto a node's own row so it roots; the seed producer's remote leg, keys live off-node) + server-local dep-trims (#152: drop hmac/pyo3-async-runtimes, lens-core async-trait/opentelemetry, base64 0.21→0.22). 0.5.78 = edge v8.6.1 / persist v12.2.0 adoption — the COMPLETE admit-node → mesh-seed loop. (1) #144 KEY-PLANE replication (KERI publish-own): the ReplicationRuntime converges an EnvelopeKind::Key coordinator per consent peer + a key_selector publishing the node's OWN key_id. (2) #150 PRODUCER: admit-node now calls persist v12.2.0 Engine::adopt_scrub_upgrade (CIRISPersist#351) to upgrade the node's own self-signed row to the accord-holder-scrubbed record — so the Key plane publishes an ANCHORED, rootable record → peers root it. Producer (server upgrades the row) + publish-own (edge advertises it) = the seed closes in place, no wipe. (3) #147 §Q B5: persist FountainHeldMeta.{content_bytes,cohort_scope} adopted (server has no fountain-content admit path; import.rs already sets cohort_scope). Verify unchanged v8.5.0. 0.5.77 = persist v12.0.2 anchor seed + accord-card UX. +version = "0.5.107" # 0.5.107 = VERIFY-FFI FOLD + CANONICAL BOOT-PRIME (triple persist v15.1.1->v15.1.2 / verify v9.0.1->v9.0.2 / edge v10.1.1->v10.1.2; 0.5.106 shipped v15.1.0/v9.0.0/v10.1.0). TWO server changes: (1) CIRISServer#232 — the agent now RIDES US for verify: ciris-verify-ffi (v9.0.1 added rlib crate-type, #187/#188) is FOLDED into ciris_server._native.so, so the ~84 ciris_verify_* C symbols live in THIS wheel's .so (no separate libciris_verify_ffi.so). Gated behind the `python` feature (only the wheel needs it, not the headless bin). The linker would --gc-sections-strip every symbol (nothing in our Rust calls them — the agent reaches them via ctypes at runtime), PER-PLATFORM-SILENTLY; kept alive by referencing verify v9.0.2's ciris_verify_ffi_link_anchor() (CIRISVerify#189) from a `#[used]` static + a black_box call in _native init. python/ciris_server/__init__.py exposes verify_ffi_path()->_native.__file__ for the agent's ffi_bindings ctypes loader. Version-skew impossible by construction (folded FFI tag == bundled substrate tag). CIRISAgent#917 then drops the standalone ciris-verify pin (kills the v9.0.0-vs-v5.1.3 wire/canonical-hash skew). VERIFIED on the actual artifact: nm -D the release cdylib lists the full ciris_verify_* surface (never trust cargo-green). (2) CIRISServer#238 — canonical boot-prime: prime_trusted_peers primes from Rooted transport_destination ROWS, but the baked canonical seed carries only a KeyRecord + an IP dial hint, no such row, so the canonical was NEVER boot-primed and rooted only via its slow announce (~130-200s, or never). An explicit-hash peer needs just (dest_hash, ed25519), BOTH deterministically derivable from the fed Ed25519 pubkey the seed already carries: dest_hash = reticulum_destination_for_pubkey(fed_ed25519) = sha256(fed_ed25519)[..16], signing_key = that same fed Ed25519 (transport+federation share the Ed25519 half; the v10.1.0 split was the unused X25519). New compose::prime_canonical_bootstrap_peers looks up each canonical's KeyRecord (lookup_public_key), derives both, and inject_rooted_peer_for_test — deterministic <30s root at boot, no announce dependency, no seed change (persist#428 closed as wrong-premise). RCA correction: I first thought priming needed the transport Ed25519 baked into the seed; the user corrected that the key is deterministically derivable from the fed key, confirmed against edge reticulum.rs (inject_rooted_peer_for_test takes no X25519). Runs in BOTH the standalone node and the agent fold. === PRIOR === 0.5.106 = TRACE DELIVERY FIX (edge v10.0.2->v10.1.0). THE delivery wheel. Minor edge bump; persist v15.1.0 / verify v9.0.0 UNCHANGED; clean pin bump (start() API stable); NO server source change. CIRISEdge#317 FIXED at the source: the announce now carries the signed TRANSPORT x25519, so the receiver admit reconstructs sha256(transport_x25519||transport_ed25519)[..16] = the EXACT identity the RNS link proves. RCA (0.5.104/0.5.105 self-diagnosing wheels): the announce advertised the node FEDERATION identity (AV-42 rooting key) but the RNS link authenticates under the TRANSPORT identity (same ed25519 signing half, different x25519 enc half), so #314 attribution compared two different keypairs -> source_key_id=None -> SkippedNoSourceKeyId -> binary CRPL hit serde_json -> schema_invalid -> Node A never replied -> 0 envelopes. NOT agent config (the announce is edge init_edge_runtime, not the serve_with_python_adapter key_id tag). Also CIRISEdge#318 (bounded peers map). END TO END: source_key_id=Some -> route_inbound_bytes -> #312 responder answers the IdentityOccurrence round -> agent resolve_peer_kex_pubkeys(canonical)=Some -> KEX PRESENT -> seals -> envelopes_sent>0 -> traces land -> CUT 2.9.7. Agent bumps ciris-server>=0.5.106 (one line) + qa_runner safety_battery --federation-delivery. FOLLOW-UP (NOT bundled, to keep this delivery wheel clean): CIRISServer#232 (ship the verify FFI so the agent drops the standalone ciris-verify pin) ships as the next wheel — a per-platform linker-symbol-export change not worth risking on the 2.9.7-critical wheel; the standalone verify at v9.x is fine meanwhile. Full suite 309/309; clippy(default+python)/fmt/gate green. === PRIOR === 0.5.105 = #317 ADMIT DISAMBIGUATOR (edge v10.0.1->v10.0.2). Patch on the 10.x line; persist v15.1.0 / verify v9.0.0 UNCHANGED; clean pin bump; NO server source change. STILL A DIAGNOSTIC WHEEL (not the delivery fix). edge v10.0.2 (CIRISEdge#317 / CIRISServer#235): the conclusive admit-time disambiguator — logs ed25519_halves_match at admit, deciding 2a (federation-identity vs transport-identity SOURCE split) vs 2b (send-vs-announce dest split) in one line. It also surfaced that EDGE LACKS THE TRANSPORT X25519 at admit (it captures the announce/federation identity, which is why the stored hash != the link-proven RNS transport identity hash sha256(x25519||ed25519)[..16] — confirming the RCA refinement: leviculum compute_hash IS combined, so the gap is the identity SOURCE, not the derivation). A rerun on 0.5.105 will still 0-envelope BUT print the verdict -> scopes the aligned-admit fix to one of three shapes (plumb transport x25519 / capture link identity / agent single-identity). THEN edge ships the fix and I adopt it + bundle CIRISServer#232 (verify FFI) in that wheel. Full suite 309/309; clippy(default+python)/fmt/gate green. Open: #317 (fix pending verdict), CIRISEdge#318 (peers-map bound). === PRIOR === 0.5.104 = SELF-DIAGNOSING edge attribution (edge v9.10.1->v10.0.1, #317 observability). Edge major 9->10; persist v15.1.0 / verify v9.0.0 UNCHANGED; clean pin bump (ReplicationRuntime::start API unchanged across v10; the v10.0.0 major is internal per-dimension replication policy realizing CIRISPersist#425). NO server source change. THIS IS A DIAGNOSTIC WHEEL, NOT THE DELIVERY FIX. CIRISEdge#317: on 0.5.103/edge9.10.1 (clean boot) the #314 attribution STILL misses — both branches false (stored transport_identity_hash != link-proven identity_hash AND announced-dest != expected-dest) → source_key_id=None → the gate skips route_inbound_bytes → binary CRPL hits serde_json -> schema_invalid -> Node A never replies -> 0 envelopes. Root cause was AMBIGUOUS (3 candidates: LinkIdentified never fired / announce-identity vs sending-link-identity split / derivation mismatch), so edge v10.0.1 ships SELF-DIAGNOSING observability (my #317 observability spec): link_attribution_miss WARN (throttled, DoS-safe) dumps all four operands + get_remote_identity(remote_identity_present) + the SkippedNoSourceKeyId/NotAReplicationFrame gate decisions + admit-time stored transport_identity_hash/dest. The match LOGIC is UNCHANGED — so a rerun on 0.5.104 will still 0-envelope BUT emit one link_attribution_miss line that pins candidate 1/2/3. THEN edge ships the actual attribution fix and I adopt it (+ bundle #232 verify-FFI per the hold-to-bundle directive). Full suite 309/309; clippy(default+python)/fmt/gate green. === PRIOR === 0.5.103 = TRACE-FLOW COMPLETE (edge v9.10.0->v9.10.1, #314 fix). Edge-only patch; persist v15.1.0 / verify v9.0.0 UNCHANGED; NO server source change. CIRISEdge#314: Node A dropped every inbound CRPL frame from the advisory-admitted agent because the inbound-link->key_id attribution matched by a RECOMPUTED dest-hash FORM (compute_destination_hash(name_hash, link_identity_hash) = named transport dest) against the peer STORED announce dest (*announce.destination_hash()); when those forms differ (named-vs-explicit, the same class as 0.5.100->0.5.101) no match -> source_key_id=None -> the edge.rs:3616 gate SKIPS route_inbound_bytes -> the binary frame falls to verify.verify()->serde_json -> schema_invalid: expected value at line 1 column 1 -> Node A never replies -> agent rounds time out -> resolve_peer_kex_pubkeys=None -> 0 envelopes. FIX (v9.10.1): attribute inbound links by transport IDENTITY (form-agnostic) not the named-dest recompute, so source_key_id populates -> route_inbound_bytes fires -> #312 auto-registers the Responder -> Node A replies with its occurrence -> agent resolves KEX -> seals -> traces flow. Field-diagnosed branch-A (source_key_id absent from the dispatch span, no no-coordinator warn). This closes the trace-flow arc END TO END: the agent (>=0.5.102, CIRISAgent#917) reruns on 0.5.103 and delivers -> cut 2.9.7. check/clippy(default+python)/fmt/gate green, full suite 309/309. === PRIOR === 0.5.102 = TRACE-FLOW UNBLOCK + TRIPLE-MAJOR (edge v9.10.0 / persist v15.1.0 / verify v9.0.0). THE trace-flow closer: edge#312 (advisory-peer RESPONDER auto-register) — Node A now answers the agent inbound anti-entropy rounds it was DROPPING at NoCoordinatorRegistered (it only built Initiator coordinators for consent peers; the agent, admitted-as-advisory, had none). The agent IdentityOccurrence round is now answered with Node A own signed occurrence -> agent resolves Node A KEX pubkeys -> seals -> traces flow. resolve_peer already addressed the advisory peer (present in self.peers w/ dest_hash, field-confirmed), so it was a one-part edge fix, NO server change for the responder. ALSO edge#311 namespace-policy replication engine + persist#425 namespace registry (CC-generated, 95 families/9 components): replication is resolved from a signed envelope namespace/cohort_scope, retiring the per-object selector whack-a-mole. SERVER adopt: ReplicationRuntime::start collapsed key_selector(#257)+occurrence_selector(#305) into ONE self_provider (yields the node key_id; the engine self-publishes Key+IdentityOccurrence+TransportDestination by namespace). ALSO Option A substrate LANDED: verify v9.0.0 (#185) — the accord co-scrub carries infra:attest via ScrubTarget.roles; the build-manifest trust root folds onto the co-scrub, retiring delegates_to. persist v15.0.0 (#422) — check_infra_attest_role_admission gates infra:attest in roles on the m-of-n accord scrub (shared verify_accord_family_coscrub w/ the canonical gate; no self-conferral). SERVER adopt: both ScrubTarget sites (admit-node + propose_canonical) carry roles: vec![] (empty = today; the future ci-key co-scrub sets ["infra:attest"]). This UNBLOCKS wiring the Trust Root trust_ci_worker card + /v1/accord/ci-key/{propose,cosign} (next). No source behavior change beyond the two adopt-fixes; full suite 309/309, clippy(default+python)/fmt/gate green. 0.5.101 named-dest recompute retained (field-proven; edge#309 local_named_dest_hash cleanup deferred). === PRIOR === 0.5.101 = SELF-OCCURRENCE NAMED-DEST FIX (unblocks inbound sealing on 0.5.100 nodes). NO substrate change (persist v14.1.0 / verify v8.12.0 / edge v9.8.0 held); server-only. RCA: 0.5.100 publish_self_identity_occurrence put edge.local_dest_hash() — the EXPLICIT hash sha256(fed_pubkey)[..16] (v7.0.0 direct-dial) — into the occurrence transport_destination, but verify_signed_identity_occurrence recomputes the NAMED hash sha256(name_hash("ciris"."edge") || sha256(x25519||ed25519)[..16])[..16] per §5.6.8.8.1.1 → DestinationHashMismatch → self-publish rejected → peers resolve None → inbound sealing blocked (field-reported on Node A). The 0.5.100 e2e BUILT the envelope with verify compute_destination_hash (the named formula) so it matched the gate by construction and never exercised the local_dest_hash() call — the bug lived only in prod. FIX: compose computes destination_hash with the gate own compute_destination_hash("ciris",["edge"],x25519,ed25519) — byte-identical to edge local_named_dest_hash (NAME_HASH_LEN=10/DEST_HASH_LEN=16, identity_hash=sha256(x25519||ed25519)[..16], x25519@[0..32]) — so the occurrence carries the named dest edge announces+listens on for mesh delivery, gate accepts, peers seal. Follow-up filed: expose Edge::local_named_dest_hash so a future release uses edge authoritative value instead of recomputing (drift-proof). === PRIOR === 0.5.100 = SIGNED OCCURRENCE-KEX (the arc 4/4 close-out: CIRISVerify#183 + CIRISPersist#418 + CIRISEdge#305/#307 adopted; CIRISServer#227 S1+S3). Substrate triple persist v13.9.1->v14.1.0 (MAJOR) / verify v8.10.1->v8.12.0 / edge v9.7.0->v9.8.0. THE GAP (user-called): content-enc was never a CUSTODY capability and the occurrence rode the wire UNSIGNED. Closed end to end: (1) verify v8.12.0 = SelfEncKeys (keyring: enc_pubkeys + kex_respond INSIDE the seal — retrieve->HKDF->scrub, no private half ever crosses an API; deterministic so restore re-derives identical keys) + produce_signed_identity_occurrence (the producer byte-matching the long-existing verify_transport_binding verifier) + by-alias FFI. (2) persist v14.0/14.1 = SignedIdentityOccurrence carries {attesting_key_id, signed_envelope, signature}; put_identity_occurrence is ONE fail-secure gate (hybrid sig over JCS envelope, dest-hash recompute §5.6.8.8.1.1, C4 transport/content-KEM separation, signer_acts_for) + LAST-SIGNED-WINS upsert (anti-first-writer poison) + put_identity_occurrence_local (trusted-local content-only device binds, NULL sig columns, EXCLUDED from replication) + list_signed_identity_occurrences_for (v14.1.0: byte-exact signed re-read — a replicator cannot re-sign, it re-wraps the signed tuple verbatim and the receiver re-verifies the SAME signature). (3) edge v9.8.0 = #305 rewired to publish from the signed re-read. SERVER: (a) compose::publish_self_identity_occurrence — boot self-publish of THIS node's SIGNED occurrence, the sealability twin of publish_self_transport_destination (transport binding = how to REACH me; occurrence = how to SEAL to me): enc pubkeys from SelfEncKeys (sealed custody, hw/sw does NOT matter), envelope carries the REQUIRED transport_destination (edge transport identity, app "ciris"/aspects ["edge"], gate recomputes the dest hash) + encryption_pubkeys, signed by the node's own hybrid signer (attesting == identity's own key). Idempotent per boot (fresh asserted_at supersedes). THE AGENT DOES NOTHING — the node self-publishes its sealability; the agent's only remaining op is the by-alias custody respond. (b) bind_occurrence_core -> put_identity_occurrence_local (content-only DEK-cascade binds; never signed-replicate). (c) tests/occurrence_kex_e2e.rs REWRITTEN to the test it should have been (the QA lesson: the 0.5.99 version fixtured the raw-seed + unsigned assumptions and passed by construction): sealed-custody fixture (SelfEncKeys by alias; raw seed touched ONLY at mint-time adopt), forged occurrence (registered-but-unrelated signer claiming another identity) REJECTED, tampered envelope REJECTED, byte-exact signed replication re-verified at the receiver, trusted-local rows excluded from the wire, rotation last-signed-wins + stale-replay no-op, seal round-trip with kex_respond INSIDE custody. HELD BACK deliberately: IdentityOccurrenceRevocation carriage (#227 S2) — the revocation half is STILL UNSIGNED in persist v14.1.0 ({identity_occurrence_revocation}, no sig container); an unauthenticated wire revocation = any consented peer can kill any identity's sealability (DoS). Flagged on CIRISPersist#418; wire the kind only when signed. === PRIOR === 0.5.99 = OCCURRENCE-KEX PLANE (CIRISEdge#305 adoption) — the last trace-flow domino: a rooted peer can finally be SEALED to. Edge bump v9.6.1->v9.7.0 (persist v13.9.1 / verify v8.10.1 floor UNCHANGED). RCA: content sealing needs the peer occurrence encryption_pubkeys (x25519+ML-KEM) that resolve_peer_kex_pubkeys reads from federation_identity_occurrences — a DIFFERENT directory object + replication plane (EnvelopeKind::IdentityOccurrence) than the transport binding 0.5.98 roots. Edge#305 (v9.7.0) added the publish-own occurrence_selector (KEX analogue of the #257 key_selector): list_identity_occurrences now honors it instead of only fanning out over the cohort, so a node advertises its OWN occurrence (enc keys). SERVER companion: (1) build_replication_peers + replication_reconcile add the EnvelopeKind::IdentityOccurrence coordinator per peer (the plane was never exchanged — only Attestation+Key) so anti-entropy carries occurrences both ways; (2) start_replication_runtime passes an occurrence_selector yielding the node key_id (publish-own). DIVISION (agent=node=one keypair): the AGENT derives the node content-enc keypair from the node Ed25519 seed at mint, publishes its self-occurrence via put_identity_occurrence, and uses the private halves in federation_session_respond; the SERVER only replicates (never holds content-enc keys / never seals). Once both ends run this, the agent resolve_peer_kex_pubkeys(canonical) flips None->Some, the trace seals, envelopes_sent>0. Arc: rootable(#397)->auto-dial(#296/#404)->survives-restart(#411/#299)->first-rootable(#301/#413)->agent-fold(#221)->advisory-durable(#416)->canonical-prime(0.5.98)->occurrence-KEX(#305). === PRIOR === 0.5.98 = CEG-NATIVE TRUSTED-PEER BOOT PRIME (CIRISServer#221 companion) — roots the explicit-hash canonical for OUTBOUND delivery. NO substrate change (edge v9.6.1 / persist v13.9.1 / verify v8.10.1 held); server-only. RCA: the explicit-hash canonical (ciris-canonical-1, v7.0.0, Leviculum ExplicitHashCannotAnnounce) is the OUTBOUND target the node dials at :4242 — it never self-roots via the inbound admit-advisory path (#301), so it must be rooted out-of-band via prime_peer. start_federation_delivery primed it on the embedded edge, but compose::serve_with_adapter (the seam the agent fold #221 runs) never did → in the folded node knows_peer(canonical)=false → anti_entropy_round coordinator error → envelopes_sent=0 (field-reported). FIX: compose now runs prime_trusted_peers on boot — CEG-native, NO hardcoded canonical: it primes EVERY transport_destinations row with BindingProvenance::Rooted (authoritative — federation-key-signed occurrence / root_binding-verified / self-published/replicated). Advisory rows (#301, routing hints) are skipped; an operator who untrusts a canonical/community withdraws the row → not primed → trust is pure CEG state load. On first boot the Rooted set IS the baked canonicals (their only rooting path, since they can't announce). Mirrors start_federation_delivery's prime but driven by the full directory (list_all_transport_destinations, #411), so the node-composed runtime — the fold — roots its trusted peers. Best-effort (missing/undecodable binding or transport-less build warns+skips). Runs in both the standalone node and the fold (shared edge). NOW: fold boot → prime the Rooted canonical → knows_peer=true → KEX → envelopes ship. 0.5.97 = ADVISORY-BINDING PERSISTENCE (CIRISPersist#416) — closes the last durable gap in the trace-flow arc. Substrate bump persist v13.9.0→v13.9.1 / edge v9.6.0→v9.6.1 (verify v8.10.1 unchanged). PURE substrate adoption, NO server source change. CIRISPersist#416 (v13.9.1, migration V101): drops the transport_destinations FK to federation_keys. RCA: V078 declared occurrence_key_id NOT NULL REFERENCES federation_keys(key_id) ("no address for an unknown key"), but V100/#413's admit-advisory model (CC 3.3.6.2) RECORDS a self-consistent announce whose key is NOT in federation_keys (authority not established) — so the FK rejected exactly the advisory row it was built to record. Field-proven in the 0.5.96 agent fold (CIRISServer#221): Node B (ciris-status-1) rooted as Advisory via edge#301 → admitted in-memory but `put_transport_destination: FOREIGN KEY constraint failed / provenance=Advisory` → binding was session-only (died on restart, blocked resolve_peer_kex_pubkeys → 0 envelopes). V101 recreates the table without the FK; the binding_provenance tag + routing-time preference (prefer Rooted; content gates on trust, CC 6 N1) are the replacement correctness. Rooted writes unaffected (their keys are in federation_keys regardless). edge v9.6.1 = coherence re-pin of persist v13.9.1. NOW: an advisory cold-start-rooted peer admits + KEXes + DURABLY PERSISTS + survives restart → boot-loads (#411/#299) → resolve_peer_kex_pubkeys resolves the x25519 → KEX → envelopes ship. THE TRACE-FLOW ARC IS COMPLETE END TO END: rootable (#397) → auto-dial (#296 + :4242 #404) → survives-restart (#411/#299) → first-rootable (#301/#413) → agent-fold (#221) → advisory-durable (#416). 0.5.96 = AGENT-FOLD SEAM (CIRISServer#221) — the last domino for trace-flow. NO substrate change (edge v9.6.0 / persist v13.9.0 / verify v8.10.1 held). serve_with_adapter (the seam `serve_with_python_adapter` folds the Python brain onto for the federation/self/accord routes on 4243) now REUSES the agent's in-process embedded engine + edge instead of building fresh: (1) build_engine → if current_rust_engine() is Some (the brain installed the persist process-singleton via Engine(config)), reuse it — the prior with_hardware_signer_hybrid opened a SECOND connection pool + sweeper on the same SQLite DSN (WAL-safe but SQLITE_BUSY-prone under a bursty cognitive loop); (2) build_edge → if embedded, reuse current_edge() (the running Arc from init_edge_runtime) instead of binding a SECOND Reticulum transport on :4242 (a hard address-in-use conflict with the agent's transport); (3) the edge.run() spawn is SKIPPED when embedded (the agent already owns the run loop). Gated on the `python` feature + current_rust_engine()==Some, so the standalone binary (serve → NoopAdapter) is byte-for-byte unchanged (embedded=false → build fresh). Slices attach + read-API mount unchanged (&Arc deref-coerces to &Edge; routing registries edge reads live per inbound frame). This lets CIRISAgent's BrainAdapter + main.py → serve_with_python_adapter fold in cleanly and additively — /v1/federation/announce on 4243 → announce → root → KEX → envelopes. CI: bundles PR #218 (sccache wired into the linux wheel legs — they were cold, no compiler cache; now ~86% warm like windows, ~15-20 min compute saved). NB caveat (boot-test verifies): several slices are documented "MUST run BEFORE edge.run()"; in the fold they run after the agent's run() — replication routing is confirmed live-OnceLock-safe, the control responder + holonomic are boot-test-to-confirm. 0.5.95 = FIRST-ROOTABLE (admit-as-advisory) — the mesh roots peers end to end. Substrate bump persist v13.8.0→v13.9.0 / edge v9.5.0→v9.6.0 (verify v8.10.1 unchanged). CIRISEdge#301 + CIRISPersist#413, grounded in CIRISConstitution CC 3.3.6.2 (an unauthenticated announce is advisory-only — a routing hint the substrate ADMITS + RECORDS + KEXes, never drops; trust is consumer-composed, not a substrate verdict) + CC 5.3.3.5 E1 (KEX = hop-by-hop path-confidentiality, a separate layer UNDER the E2E content DEK) + CC 6 N1 (trust≠membership; admission gated at the destination on CONTENT, never on the connection). THE FIX: edge's announce handler stops dropping unauthenticated announces — root_binding now CLASSIFIES (Rooted | Advisory) instead of gating. An unknown/unrooted peer is admitted as Advisory (self-signature-verified routing hint), recorded, and KEX'd — so a FRESH peer can finally first-root (the CIRISServer#216 wall). advisory→rooted upgrade on the same-epoch re-announce that roots; forged self-claims still dropped (the AV-42 dest_hash crypto floor is preserved). persist v13.9.0: TransportDestination gains binding_provenance: Rooted|Advisory (default Rooted, back-compat) so the tag is durable + boot-loaded (#411/#299); competing claims on a destination are admitted + surfaced, resolved by preference at routing time (prefer Rooted), never a substrate reject. RCA correction baked in: the prior owner-gated-admission behavior read the code's buggy drop-on-unknown as intent — the constitution says a server does NOT opt in to receiving. SERVER wiring: the 4 authoritative TransportDestination construction sites (self-publish, accord canonical address, occurrence-bind, prime-resolve fixture) tag binding_provenance: Rooted. NO other server source change — content-acceptance already gates on verify-against-directory (CC 6 N1), which composes correctly with edge admitting the advisory link. Arc COMPLETE: rootable (#397) → auto-dial (#296 + :4242 #404) → survives-restart (#411/#299) → first-rootable (#301/#413). Follow-on centipede (post-0.6, stronger control — trust-root-blessed build attestation before durable save, software-node-compatible): CIRISVerify#181 → CIRISPersist#415 → CIRISEdge#303 → CIRISServer#219. 0.5.94 = SURVIVES-RESTART + BOOT-UNBRICK (completes the trace-flow substrate arc). Substrate bump persist v13.6.1→v13.8.0 / edge v9.4.1→v9.5.0 (verify v8.10.1 unchanged). Carries the whole line end to end: (1) CIRISPersist#410 (v13.7.0) — the genesis canonical-seed re-bake no longer BRICKS boot on a node that already holds a different anchor-scrubbed record (the :4243→:4242 correction bricked Node A on 0.5.93; seed_canonical_servers now tolerates/upgrades instead of Err-aborting) → NODE A + the whole 0.5.86+ fleet can finally upgrade off 0.5.92. (2) CIRISPersist#411 + CIRISEdge#299 (v13.8.0 / v9.5.0) — SURVIVES-RESTART: the rooted-peer transport binding is now DURABLE. persist's TransportDestination gains transport_x25519_pubkey_base64 (the KEX half, pair to #397's ed25519) + list_all_transport_destinations; edge write-throughs each rooted binding (dest-hash + ed25519 + x25519) to persist on announce-root and BOOT-LOADS all bindings into the transport resolver at startup → a known peer is knows_peer=true and SEALABLE with zero announces after a restart. persist is the source of truth; boot reloads valid rooted state from persist — no re-announce, no re-peer. This is the direct fix for CIRISServer#216 (empty AV-42 registry → resolve_peer_kex_pubkeys=None → 0 envelopes sealed). SERVER wiring: publish_self_transport_destination now saves the X25519 half too (transport_pubkey[0..32]) so THIS node's own binding is fully sealable; the accord canonical address-update (CanonicalAddressUpdate) carries transport_x25519_pubkey_base64 (skip_serializing_if — prior threshold sigs stay byte-identical) so an address move preserves sealability; occurrence-bind + prime-resolve carry the new field. (3) CIRISPersist#405 (overlay honored) rides v13.8.0. NO other server source change. The trace-flow arc: rootable (#397) → auto-dial (#296 + :4242 #404) → address-move (#405/#410) → survives-restart (#299). 0.5.93 = CANONICAL PORT FIX + m-of-n REPLACE (completes the zero-delivery fix end to end). Substrate bump persist v13.6.0→v13.6.1 / edge v9.4.0→v9.4.1 (verify v8.10.1 unchanged). (1) CIRISPersist#404 (v13.6.1): the baked canonical genesis seed (src/federation/genesis/canonical_seed.json) carried the READ-API port in the signed transport hint — `108.61.242.236:4243` (listen+1, the HTTP port) instead of the Reticulum transport port `:4242`. Every fresh node (incl. the agent) auto-seeded its dial (CIRISEdge#296) from that hint → dialed :4243 → spoke RNS at the HTTP API → never rooted. RCA: the read-API port was supplied at the co-scrub ceremony (the client base URL defaults to :4243, so it's the port a human copies) and sealed into the scrub-signed registration_envelope. FIX = re-ran the 2-of-3 co-scrub (A1+B1) over the SAME identity (ciris-canonical-1-d7bdeu223k, pubkeys unchanged) with destination=:4242, persist re-baked the seed. A fresh install now dials :4242 and roots the canonical zero-touch. edge v9.4.1 = coherence re-pin of persist v13.6.1. (2) CLIENT m-of-n replace fix: the Trust Root "Supersede/replace" of an existing canonical routed to the LEGACY 1-of-N addCanonicalServer (CIRISServer#174 migrated Add→co-scrub but left replace behind) — a 1-scrub record FAILS the canonical admission gate (needs distinct_scrub_count ≥ family quorum 2) on fresh installs. Both add AND replace now go through the m-of-n co-scrub (proposeCanonical); the 1-of-N /v1/accord/canonical/add path is now unused from the UI (retire separately). NO server source change (pure substrate adoption + client). Filed CIRISPersist#405 (canonical_bootstrap_hints should honor the mutable transport_destination overlay so a future address move takes effect at runtime without a re-bake). 0.5.92 = TRACE-FLOW UNBLOCK (the release that gets federation traces flowing). Substrate bump persist v13.5.0→v13.6.0 / edge v9.3.0→v9.4.0 (verify v8.10.1 unchanged). CIRISEdge#296: init_edge_runtime now AUTO-SEEDS the canonical TCP dial from persist's baked canonical_bootstrap_hints(). RCA (3-day chase): edge seeded its dial ONLY from the caller-passed bootstrap_peers (pyo3.rs:4741) — rooting was wired + the canonical identity key baked, but the agent-embedded edge (which does NOT run compose::serve) booted with an EMPTY canonical dial → no link to the public canonical → never received its announce → the (already-wired) rooting never fired → knows_peer(canonical)=false → zero delivery. Neither the bake nor edge-init nor rooting was broken; the ONE missing piece was the "read baked hints → seed dial" glue that lived only in ciris-server's compose::serve. Now init_edge_runtime calls the engine's #402 canonical_bootstrap_hints() pyfn cross-wheel, filters kind==ip, unions the TCP dials in (dedup, fail-soft, fully logged) — EVERY direct consumer (the agent) now dials + roots the canonical mesh with zero caller glue, exactly what compose::serve does. CIRISPersist#402 (v13.6.0): canonical_bootstrap_hints() exposed to Python (PyEngine → JSON [{key_id,kind,destination}]) so edge reads a clean hint list instead of parsing signed envelopes. NO server source change — pure substrate adoption; compose::serve's explicit canonical_bootstrap_addrs union is now redundant-but-harmless (dedup covers it), retire next release. ALSO bundles the CI windows-sccache warm (PR #211: warm-release-cache.yml restored, substrate-bump-gated — v0.5.91 windows was 100% cold, proven 0% sccache hits / 445 misses / 21min, because nothing warmed the maturin sccache on main). 0.5.91 = ZERO-TOUCH DELIVERY + CI TRIM (no substrate change; triple stays persist v13.5.0 / edge v9.3.0 / verify v8.10.1). (1) BOOT SELF-PUBLISH (#205 gap #2, the durable close): compose now publishes THIS node's reticulum transport-tier binding (dest_hash from edge.local_dest_hash() + the transport Ed25519 from edge.local_transport_pubkey()[32..64]) into the federation directory at boot, for its OWN key_id — so a v7.0.0 explicit-hash canonical (which cannot announce) is primeable straight from the directory with ZERO operator action: restarting Node A on 0.5.91 IS the publish (the row replicates → a consuming node's start_federation_delivery resolves + prime_peer-roots it → knows_peer flips → delivery converges). Best-effort, self-authenticating (own key_id + own edge-owned transport identity, same basis as an attested announce), no accord quorum needed to declare own reachability; silent no-op on a non-reticulum node. (2) CI-REDUNDANCY TRIM: ci.yml + localization.yml are now pull_request-only — a squash-merge is byte-identical to the PR that just passed, and neither holds a CIRISCache save (that lives in conformance.yml, still on main), so the main-push re-run was pure duplication. conformance's main run (save) + the tag's publish conformance-gate (validates the actual published wheel) are kept — not pure dups. 0.5.90 = TRANSPORT-PRIME (delivery close-out). Substrate triple persist v13.4.2→v13.5.0 / edge v9.2.0→v9.3.0 / verify v8.10.0→v8.10.1. (1) rust-toolchain.toml EXACT-pin stable→1.97.0 (CIRISEdge#291) — `stable` floats, so the CIRISCache key `-rustc-` never matched the substrate repos exact pin → windows/linux wheel restored COLD; aligning all four repos on 1.97.0 makes the restore hit the substrate layer. (2) persist v13.5.0 #397: TransportDestination gains transport_ed25519_pubkey_base64 (Option) — the transport-tier Ed25519 that pairs with a reticulum dest-hash so a peer can prime_peer an explicit-hash canonical (which cannot announce). The accord canonical-address-update invocation (CanonicalAddressUpdate) now carries it (skip_serializing_if keeps pre-#397 threshold sigs byte-identical); the generic occurrence-bind leaves it None. (3) start_federation_delivery now prime_peers the admitted explicit-hash canonicals from the resolved directory binding (gap #1 of #205) + edge v9.3.0 rooting observability (#292). (4) 1.97 clippy: lens-core manual Option::filter → .filter(). 0.5.89 = FEDERATION DELIVERY (#205, subsumes #204). edge v9.1.7→v9.2.0 (CIRISEdge#289: current_edge() downstream-public + require_local_signer attested-announce; persist v13.4.2 / verify v8.10.0 unchanged) + `ffi-uniffi` on the edge dep so ciris_edge::current_edge() links into the controller. NEW pyo3 entry `start_federation_delivery(cadence_seconds=None, announce_logger=True) -> int` (src/federation_delivery.rs + src/lib.rs): the bare AGENT edge, after init_edge_runtime, calls ONE thing that runs the compose delivery controller in-process against current_rust_engine()+current_edge() — reads the baked canonical transport_hints→targets (subsumes #204's read), authors this node's directed consent:replication grant per admitted canonical peer, starts the ONE ReplicationRuntime seeding the canonical key_ids, installs inbound routing (safe post-boot — Edge::run reads the routing OnceLock live per frame), spawns the consent reconcile loop (set_peers) + announce logger. Compose boot UNCHANGED — factored setup_peer_replication's core into compose::start_replication_runtime(engine,edge,node_key_id,extra_targets) (compose passes &[], the controller passes canonical key_ids) + a shared build_replication_peers. CONTRACT: init_edge_runtime(engine=from_shared_with_local(...), disable_reticulum=False, require_local_signer=True, announce_interval_seconds=15) → start_federation_delivery(). CAVEAT: the Reticulum transport only adds a TCP *dial* peer at BUILD time (no runtime add-peer), so the canonical IP MUST be in the edge init bootstrap_peers (agent-side, the user's peers=1 path) — the controller drives everything downstream of the dial. Live delivery is the agent live-lens QA (not unit-testable: needs a live edge + Node A); 4 new unit tests cover peer-set assembly / dedup / empty sets / uninitialized-guard. 0.5.88 = NODE-A BOOT FIX (triple adoption). Substrate bump persist v13.4.1→v13.4.2 / edge v9.1.6→v9.1.7 / verify v8.9.0→v8.10.0. CIRISPersist#394: the #390 canonical bake made seed_canonical_servers `put_public_key` the baked 2-of-3 record — but on the CANONICAL NODE ITSELF (ciris-canonical-1-d7bdeu223k already exists as its own self-signed `node` row, same pubkeys, minted at first registration), put_public_key saw same-key/different-content → Err → EngineError::GenesisSeed → BOOT ABORT. The one node the bake was meant to serve couldn't boot on v13.4.0/v13.4.1 (RCA'd here; bisected 0.5.85✅/0.5.86❌). v13.4.2 seeds via adopt_scrub_upgrade instead: absent→insert, self-signed same-pubkey→UPGRADE to the scrubbed canonical record (A self-roots), already-scrubbed→no-op, drift→reject. verify v8.9.0→v8.10.0 (persist v13.4.2 requires it): additive — the new `ciris-verify manifest sign` CLI (#176/#177, the canonical build-manifest producer emitting SignedCegObject/build_manifest_contribution scoped infra:attest); no server consumer break. edge v9.1.7 = coherence re-pin of persist v13.4.2 + verify v8.10.0. NO server source change — pure substrate adoption; unblocks Node A's upgrade off 0.5.85. 0.5.87 = PYO3-SEED FIX ADOPTION. Substrate bump persist v13.4.0→v13.4.1 / edge v9.1.5→v9.1.6 (verify v8.9.0 unchanged). CIRISPersist#392: the pyo3/wheel ctor PyEngine::new hand-rolled its own genesis seed and stopped at the accord holders — it never ran the #386 entrenched-family (v13.3.0) or #390 canonical (v13.4.0) bakes that Engine::with_signer does. So EVERY wheel consumer (the server's own py_main boot + agent-embedded engines, CIRISServer#191 / CIRISAgent#896) got A1/B1/C1 but NO HUMANITY_ACCORD family row and list_canonical_servers()==[] — the canonical bake never reached the installs it was meant to light up. v13.4.1 factors the full post-holders sequence (verify anchor → family #386 → canonical #390) into ONE shared genesis::seed_family_and_canonical(dir) called by BOTH ctors, so pyo3 + Rust engines are seed-identical. edge v9.1.6 = coherence re-pin of persist v13.4.1. NO server source change — pure substrate adoption; a fresh wheel Engine now returns ciris-canonical-1 + an entrenched family. 0.5.86 = GENESIS-BAKE + GRAPH-FIX. Substrate bump persist v13.3.1→v13.4.0 / edge v9.1.4→v9.1.5 (verify v8.9.0 unchanged). (1) CIRISPersist#390/#391: the 2-of-3 canonical genesis server (ciris-canonical-1-d7bdeu223k) is now BAKED at genesis — the operator's live accord-co-scrubbed record (A1 primary + B1 in additional_scrubs, field-conferred this session) restores the seed #383 deferred (a 1-of-N founding record was a first-strike weakness; canonical ADD is 2-of-3, so the baked genesis is too). A FRESH install boots already trusting ciris-canonical-1 (adopt_scrub_upgrade seeds/upgrades every node's row incl. Node A's own → A self-roots on upgrade), nodes addressable by key_id — no ceremony. edge v9.1.5 = coherence re-pin of persist v13.4.0 (same-repo [patch] gotcha). (2) GRAPH-VIEW FIX: the memory graph crashed loading (JsonConvertException: NodeType has no 'attestation') — 0.5.85's seed_ceg_graph projects CEG trust-root kinds (owner/owned_node/canonical_server/family/holder/delegation/attestation/peer) the client's strict NodeType enum didn't know, so ONE unknown value failed the whole /v1/memory/timeline payload. Fix (client): added the 8 CEG variants + a TOLERANT NodeTypeSerializer (unknown→UNKNOWN, never throws again) + distinct CEG-plane colors. 0.5.85 = SEED-UX cut (no substrate change; edge v9.1.4 / persist v13.3.1 / verify v8.9.0). The fixes that let the operator + a second holder seed the canonical mesh cleanly from the desktop UI: (1) OS-AWARE ykcs11 module path — the node's default PKCS#11 module now resolves per-OS (macOS Homebrew .dylib Apple-Silicon/Intel; Windows Yubico PIV Tool .dll / PATH; Linux .so) via identity.rs::default_ykcs11_module, wired into Pkcs11Options + the accord propose/cosign/admit paths, so a macOS/Windows holder's YubiKey cosign works (was hardcoded Linux → dlopen failure). Optional per-request "PKCS#11 module path (advanced)" field in the hardware-scrub sheet as an override. (2) COPY/EXPORT the co-scrub partial + finished record from the Trust Root card (Propose/Cosign/Pending) — Copy-to-clipboard + Save-to-file; no more pulling JSON off disk. (3) NAV label "Accord" → "Trust Root". (4) CIRISServer#127++ — the memory graph now PROJECTS persist's CEG state (seed_ceg_graph): ~40+ nodes (identity, owner, owned nodes, HUMANITY_ACCORD family + holders, canonical servers, config:*, delegations/consent/structural attestations) with CEG-native typed edges (delegates_to owner-binding, has_member family→holder seat, scrub_conferral holder→canonical, has_config, replicates_to, authored, supersedes/withdraws/recants); each node carries kind·subject·status·record so the client can render it as an AttestationCard. Follow-ups: wire the Trust Root ⋮ hamburger onto graph nodes (client); 28-language fan-out for the 7 new strings; pending-co-scrub graph node. 0.5.84 = THE FULL SAFE-MESH-SEED CUT. Substrate lockstep bumped edge v9.1.2→v9.1.4 / persist v13.2.0→v13.3.1 (v13.3.1 #387 adds the test-only with_signer_no_genesis_seed seam so the accord ceremony tests get a clean engine) (verify v8.9.0 unchanged). (1) CIRISPersist#386: the HUMANITY_ACCORD federation_families row (quorum:2/3, A1/B1/C1) is now SEEDED at boot on every node (idempotent) — lookup_family resolves durably, so the 0.5.83 baked-genesis fallbacks are RETIRED (recognized_family_from_baked_genesis + family_quorum_m fallback deleted; get_family reads the real entrenched row; V097 drops the family_key_id→federation_keys FK — a constitutional family is keyless). (2) CIRISServer#181 producer hook: POST /v1/safety/flag (duty-gated: verify_request + admit_moderation_action(Moderate)) emits the substrate-reserved content_class:{infohazard|reported} flag via a NEW node-scoped substrate_persist identity (sealed Ed25519 -substrate + software ML-DSA seed, registered through register_federation_key) — the duty-holder authorizes, the substrate signs; action:clear supersedes. This makes the #161 infohazard reveal gate actually FIRE (flag → GET /v1/safety/reveal 403 interstitial for a non-consented viewer). (3) CIRISServer#161 (already on main): POST /v1/safety/reveal — the CC 4.5.13 consent gate. #378 single-owner marker gate rides the persist bump (no server action). Follow-ups: retire ensure_accord_family_anchor throwaway key (low-risk, legacy assemble path); infohazard consent lifecycle + interstitial UX (#180 umbrella / #182-#185). 0.5.83 = BAKED-FAMILY RECOGNITION + RAISE-A-HALT. Same substrate (edge v9.1.2 / persist v13.2.0 / verify v8.9.0). Fixes "No accord family established on this node yet" on a node that has the BAKED genesis but no entrenched federation_families row (persist seeds the 3 holder KEY rows, not the family row — durable fix filed CIRISPersist#386): GET /v1/accord/family + the co-scrub quorum_needed now fall back to verify's baked humanity_accord_genesis() (the 2/3 family + 3 seats, entrenched=false, recognized_via=baked-genesis) — the same recognition resolve_kill_switch_roster already uses. PLUS raise-a-halt: POST /v1/accord/halt (initiate_halt, the binding twin of /drill — one opener signature is sub-quorum and cannot latch; 2-of-3 concur latches) + the client "Halt the mesh" action (was a disabled placeholder). 0.5.82 = CO-SCRUB END-TO-END. Same substrate lockstep edge v9.1.2 / persist v13.2.0 / verify v8.9.0 (no triple change). The cross-device m-of-n seed now works from the app: the co-scrub partial GOSSIPS over the accord peer-plane (the same HTTP set the kill-switch uses) — propose on A1's box → the partial surfaces under "Pending co-signs (canonical)" on B1's box → cosign → conferred, no manual transfer. Server (src/accord_provision.rs): ProvisionState gains an ephemeral bounded pending store + a (target,scrub-count) seen-set; ingest_partial validates structurally + upserts + gossips; NEW open (non-loopback) POST /v1/accord/canonical/gossip-partial peer-receive + GET /v1/accord/canonical/pending; router() split into build()→{loopback,gossip}. Client (#174): proposeCanonical/cosignCanonical/listPendingCoscrubs, the "Pending co-signs (canonical)" section + CanonicalCosignSheet (pending-entry or pasted-partial fallback), "[+ New] Add"→"Propose a canonical server". Bundled: #117 (Delegations loads on screen entry, not VM init → no pre-login 401) + #134 (existing-key offer no longer gated on a typed label; label auto-populates from the picked key_id). 0.5.81 = M-OF-N TRUST ROOT. Substrate lockstep edge v9.1.2 / persist v13.2.0 / verify v8.9.0 (CIRISPersist#383: KeyRecord.additional_scrubs + the DYNAMIC m-of-n admission gate via verify's verify_quorum_policy — canonical is conferred iff distinct_scrub_count() ≥ the family's entrenched quorum:M/N, NOT a hardcoded 2; the 1-of-N genesis is no longer auto-baked). Trust Root co-scrub ceremony (server-side, hardware-signed via the shared open_holder_identity custody path): POST /v1/accord/canonical/{propose,cosign} (scrub #1 → gossip/transfer the partial → append_scrub over the byte-identical envelope → adopt_scrub_upgrade lets the m-of-n gate confer/hold), plus /withdraw + /supersede (#377, 2-of-3 via proposal_digest) and IP-in-envelope TransportHint. Accord ops drill + announce (/v1/accord/{drill,announce}, node-synth'd invocation, gossiped, recorded pre-latch) + /halt-status. DRY audit landed (recipes routed through the substrate: build_self_key_record→produce_self_key_record, CANONICAL_BOOTSTRAP_PEERS→Engine::canonical_bootstrap_hints, owner_binding consts aliased). CEG-native Trust Root card redesign (one AttestationCard + ⋮ hamburger; concur now hardware-signs). KNOWN → 0.5.82 (#174): the client "Add canonical" still posts /add (1-scrub partial under m-of-n) — wire the propose→cosign buttons. 0.5.80 = THE MESH-SEED RELEASE. Substrate lockstep edge v9.1.0 / persist v13.0.1 / verify v8.7.0 (CC 1.0 RC1: KeyRecord.consent_role, the accord-conferred `canonical` identity_type role + admission gate CIRISPersist#372, single-owner owner_of #162, CIRISEdge#277 upgrade-aware Key-plane apply → KERI publish-own end-to-end). Trust Root canonical ops (#164): POST /v1/accord/canonical/add — the 1-of-N hardware-scrub SEED op (scrub-sign w/ the canonical role + adopt onto own row + publish address) + GET /v1/accord/canonical/servers + the client "Canonical servers" card (update-address shipped #165; supersede/withdraw → 0.5.81, need a persist role-removal primitive). ci.yml revived (my #155 evidence-step colon had broken the Rust gate repo-wide). 0.5.79 = substrate triple edge v8.7.2 / persist v12.5.0 / verify v8.7.0 (cross-cdylib block_on-deadlock/SIGABRT fix + CC 0.9.3 gates + dep-trims landed: hickory 0.25 de-dup, dead openssl/qrcodegen/bip39 gone) + REMOTE adopt-scrubbed endpoint (#150 — POST /v1/federation/adopt-scrubbed applies an A1-scrubbed record onto a node's own row so it roots; the seed producer's remote leg, keys live off-node) + server-local dep-trims (#152: drop hmac/pyo3-async-runtimes, lens-core async-trait/opentelemetry, base64 0.21→0.22). 0.5.78 = edge v8.6.1 / persist v12.2.0 adoption — the COMPLETE admit-node → mesh-seed loop. (1) #144 KEY-PLANE replication (KERI publish-own): the ReplicationRuntime converges an EnvelopeKind::Key coordinator per consent peer + a key_selector publishing the node's OWN key_id. (2) #150 PRODUCER: admit-node now calls persist v12.2.0 Engine::adopt_scrub_upgrade (CIRISPersist#351) to upgrade the node's own self-signed row to the accord-holder-scrubbed record — so the Key plane publishes an ANCHORED, rootable record → peers root it. Producer (server upgrades the row) + publish-own (edge advertises it) = the seed closes in place, no wipe. (3) #147 §Q B5: persist FountainHeldMeta.{content_bytes,cohort_scope} adopted (server has no fountain-content admit path; import.rs already sets cohort_scope). Verify unchanged v8.5.0. 0.5.77 = persist v12.0.2 anchor seed + accord-card UX. edition = "2021" # MSRV floor is set by the substrate: ciris-verify v5.2.0 requires 1.86 # (persist v6.0.1 is 1.83). Build with the higher of the two. @@ -54,6 +54,9 @@ default = ["pkcs11"] # ctypes.CDLL), not a `#[pymodule]`, so it cannot be a pyo3 submodule. python = [ "dep:pyo3", + # Fold the verify FFI C surface into `_native.so` (CIRISServer#232) — only the + # wheel the agent loads needs it, so it rides the `python` feature, not the bin. + "dep:ciris-verify-ffi", "ciris-lens-core/python", "ciris-persist/pyo3", "ciris-edge/pyo3", @@ -122,8 +125,8 @@ ciris-lens-core = { path = "crates/ciris-lens-core" } # ABSORBED in-tree (wo # `cirislens_wa_cert` + `cirislens_service_token_revocation` expose the auth # substrate the fabric absorbs (CIRISServer#9, src/auth): the agent's `wa_cert` # table (users/WA/OAuth/api-keys) + the `revoked_service_tokens` table. -ciris-persist = { git = "https://github.com/CIRISAI/CIRISPersist", tag = "v15.1.0", features = ["sqlite", "cirisgraph", "cirislens_wa_cert", "cirislens_service_token_revocation", "cirisaudit", "cirislens_tasks", "cirislens_thoughts", "cirislens_tickets", "cirislens_correlations", "cirislens_deferral_reports", "cirislens_maintenance_locks", "cirislens_creation_ceremonies", "cirislens_legacy_migration", "cirisincident", "telemetry", "secrets"] } -ciris-edge = { git = "https://github.com/CIRISAI/CIRISEdge", tag = "v10.1.0", features = ["transport-reticulum", "transport-http", "transport-packet-radio", "ffi-uniffi"] } +ciris-persist = { git = "https://github.com/CIRISAI/CIRISPersist", tag = "v15.1.2", features = ["sqlite", "cirisgraph", "cirislens_wa_cert", "cirislens_service_token_revocation", "cirisaudit", "cirislens_tasks", "cirislens_thoughts", "cirislens_tickets", "cirislens_correlations", "cirislens_deferral_reports", "cirislens_maintenance_locks", "cirislens_creation_ceremonies", "cirislens_legacy_migration", "cirisincident", "telemetry", "secrets"] } +ciris-edge = { git = "https://github.com/CIRISAI/CIRISEdge", tag = "v10.1.2", features = ["transport-reticulum", "transport-http", "transport-packet-radio", "ffi-uniffi"] } # Founder-quorum verification + key-id fingerprint at the composition root # (threshold:: module path — NOT re-exported at the crate root). # LOCKSTEP: persist v9.4.0 transitively pins verify-core v6.6.x — the whole @@ -132,16 +135,27 @@ ciris-edge = { git = "https://github.com/CIRISAI/CIRISEdge", tag = "v10.1 # types. v6.6.1 (CIRISVerify#89) adds the create_federation_identity `seal_alias` # arg the #247 user-key-derived fix consumes. (6.4–6.6 are HUMANITY_ACCORD + STH # stack — the accord-genesis surface CIRISServer#41 consumes.) -ciris-verify-core = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.0" } +ciris-verify-core = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.2" } # The hybrid crypto primitives (Ed25519 + ML-DSA-65 software signers) — used by # the portable software identity occurrence to build a `HybridSigningIdentity` # over two software seeds. Same tag as the rest of the verify family. -ciris-crypto = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.0", version = "9", features = ["pqc-ml-dsa", "self-enc"] } +ciris-crypto = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.2", version = "9", features = ["pqc-ml-dsa", "self-enc"] } # Hardware-backed transport-identity keystore (verify v5.2.0 #68 / edge #99). # Base = software keystore + byte-identical migration of an existing .rid (works # everywhere, no libtss2). The real TPM-2.0 / SE / StrongBox backend rides the # `tpm` feature; runtime auto-detects hardware and falls back to software. -ciris-keyring = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.0", version = "9", features = ["software", "pqc-ml-dsa"] } +ciris-keyring = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.2", version = "9", features = ["software", "pqc-ml-dsa"] } +# The verify FFI (C ABI) — folded into `ciris_server._native.so` (CIRISServer#232) +# so the agent drops its standalone `ciris-verify` wheel and rides us for verify +# too (kills the v9.0.0-vs-v5.1.3 wire/canonical-hash skew, CIRISAgent#917). v9.0.1 +# added `rlib` to its crate-type so we can link it; v9.0.2 added +# `ciris_verify_ffi_link_anchor()` (CIRISVerify#189) — we reference it via `#[used]` +# in src/lib.rs so `--gc-sections` cannot dead-strip the ~84 `ciris_verify_*` C +# symbols out of our cdylib. Same tag as the rest of the verify family → the folded +# FFI version == the bundled substrate version by construction (no skew possible). +# Default features (secp256k1, key-grant, hybrid-kex) + the crypto features unify +# with ciris-crypto's above (self-enc, pqc-ml-dsa) across the graph. +ciris-verify-ffi = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.2", version = "9", optional = true } # PyO3 bindings for the abi3 wheel — optional, gated behind the `python` feature # so the binary never links them. 0.29 matches the persist/edge family floor. @@ -204,16 +218,16 @@ tracing-appender = "0.2" # - keyring `tpm` (tss-esapi / TPM 2.0) — the real hardware backend; needs # libtss2 (Linux build dep). Other targets keep the software keystore. [target.'cfg(target_os = "linux")'.dependencies] -ciris-persist = { git = "https://github.com/CIRISAI/CIRISPersist", tag = "v15.1.0", features = ["postgres", "cirisgraph", "cirislens_wa_cert", "cirislens_service_token_revocation", "cirisaudit", "cirislens_tasks", "cirislens_thoughts", "cirislens_tickets", "cirislens_correlations", "cirislens_deferral_reports", "cirislens_maintenance_locks", "cirislens_creation_ceremonies", "cirislens_legacy_migration", "cirisincident", "telemetry", "secrets"] } +ciris-persist = { git = "https://github.com/CIRISAI/CIRISPersist", tag = "v15.1.2", features = ["postgres", "cirisgraph", "cirislens_wa_cert", "cirislens_service_token_revocation", "cirisaudit", "cirislens_tasks", "cirislens_thoughts", "cirislens_tickets", "cirislens_correlations", "cirislens_deferral_reports", "cirislens_maintenance_locks", "cirislens_creation_ceremonies", "cirislens_legacy_migration", "cirisincident", "telemetry", "secrets"] } # keyring `tpm` (TPM-at-rest, Linux-only via tss-esapi). `pkcs11` is no longer # per-target — it rides the global `pkcs11` feature now that cryptoki builds on all # targets (CIRISVerify v6.12.0). -ciris-keyring = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.0", version = "9", features = ["tpm-plugin"] } +ciris-keyring = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.2", version = "9", features = ["tpm-plugin"] } # Android (arm64/arm32/x86_64): the keyring `android` backend (JNI → Android # Keystore/StrongBox); sqlite bundles, no postgres/tpm. Mirrors persist/edge. [target.'cfg(target_os = "android")'.dependencies] -ciris-keyring = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.0", version = "9", features = ["android"] } +ciris-keyring = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.2", version = "9", features = ["android"] } # serialport (the RNode LoRa driver dep, src/radio.rs) pulls libudev-sys on Linux, # which only resolves where libudev exists: desktop/server Linux (gnu, x86_64 or @@ -264,20 +278,20 @@ http-body-util = "0.1" # The PQC primitives the realtime-A/V E2E bench drives directly: hybrid KEX # (X25519 + ML-KEM-768) for the per-Link transit key, AES-256-GCM for the # two-layer chunk seal. Same pin/features edge composes (tag parity). -ciris-crypto = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.0", version = "9", features = ["hybrid-kex", "aes-gcm", "pqc-ml-dsa", "self-enc"] } +ciris-crypto = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.2", version = "9", features = ["hybrid-kex", "aes-gcm", "pqc-ml-dsa", "self-enc"] } # Enable edge's OWN fountain codec (`codec-fountain`, L1-A) for the survival-floor # proof — dev/test ONLY (feature unification applies to test/bench builds; the # shipped wheel stays codec-free, since a relay forwards sealed symbols opaquely # and never encodes/decodes). This lets tests/chaos_mesh.rs exercise the SUBSTRATE # codec (`fountain_encode`/`fountain_decode`) directly — a real MEASURED proof, # not a reference stand-in. -ciris-edge = { git = "https://github.com/CIRISAI/CIRISEdge", tag = "v10.1.0", features = ["codec-fountain"] } +ciris-edge = { git = "https://github.com/CIRISAI/CIRISEdge", tag = "v10.1.2", features = ["codec-fountain"] } # Enable persist's TEST-ONLY genesis-seam (`test-genesis-seam`, CIRISPersist#387) for the # accord ceremony tests (tests/accord.rs): `Engine::with_signer_no_genesis_seed` yields a # clean engine with the baked HUMANITY_ACCORD family seed SKIPPED, so those tests can stand # up their OWN custom-holder family via the assemble ceremony. Dev/test ONLY — resolver-2 # keeps this feature out of the shipped lib/bin/wheel (prod always seeds the baked family). -ciris-persist = { git = "https://github.com/CIRISAI/CIRISPersist", tag = "v15.1.0", features = ["test-genesis-seam"] } +ciris-persist = { git = "https://github.com/CIRISAI/CIRISPersist", tag = "v15.1.2", features = ["test-genesis-seam"] } # PQC realtime-A/V streaming E2E benchmark (CIRISEdge#62 realtime_av profile). [[bench]] @@ -344,3 +358,27 @@ path = "examples/mesh_propagation/main.rs" panic = "unwind" strip = false # release workflow strips per-target after build +# ── Build-memory guardrail (do NOT remove without reading this) ─────────────── +# `cargo test` does not build one binary — it links the ~32 integration-test +# binaries in tests/*.rs, and EACH statically links the FULL substrate (persist + +# edge + verify + the pyo3 cdylib and all their deps). At the dev/test default of +# full DWARF (`debug = true`) that is ~2-3 GB of peak RSS per link, and cargo runs +# `nproc` jobs by default (32 on this box) → ~32 simultaneous multi-GB links, tens +# of GB over the 31 GiB of RAM. The kernel does not OOM-kill it (no `oom-kill` in +# the log); it swap-THRASHES and the machine goes unresponsive. That is exactly +# what happened once — hence this pin. +# +# `line-tables-only` keeps file:line in panic backtraces (all the suite actually +# needs) while dropping the local-variable DWARF that dominates link memory — +# roughly a 5-10x cut in per-link RSS, so even full-width parallelism fits. It +# also makes CI links faster and lighter, so it is safe on small runners (unlike a +# `[build] jobs` cap, which would OVER-subscribe a 4-core runner). +# +# Stepping through code in a debugger? Override per-invocation, don't edit this: +# cargo test --config 'profile.dev.debug=2' --config 'profile.test.debug=2' +[profile.dev] +debug = "line-tables-only" + +[profile.test] +debug = "line-tables-only" + diff --git a/crates/ciris-lens-core/Cargo.toml b/crates/ciris-lens-core/Cargo.toml index 2aba371a..8656d782 100644 --- a/crates/ciris-lens-core/Cargo.toml +++ b/crates/ciris-lens-core/Cargo.toml @@ -25,8 +25,8 @@ crate-type = ["cdylib", "rlib"] # # OQ-01 closure 2026-05-03: rlib primary, PyO3 cdylib via `python` # feature for the deployed-lens cutover. -ciris-persist = { git = "https://github.com/CIRISAI/CIRISPersist", tag = "v15.1.0", version = "15", features = ["extract", "sqlite"] } -ciris-edge = { git = "https://github.com/CIRISAI/CIRISEdge", tag = "v10.1.0", version = "10", features = ["transport-http", "transport-reticulum"] } +ciris-persist = { git = "https://github.com/CIRISAI/CIRISPersist", tag = "v15.1.2", version = "15", features = ["extract", "sqlite"] } +ciris-edge = { git = "https://github.com/CIRISAI/CIRISEdge", tag = "v10.1.2", version = "10", features = ["transport-http", "transport-reticulum"] } # Keyring — relay mode (CIRISLensCore#10) loads its Edge transport- # signing identity via `ciris_keyring::load_local_seed`. Edge's @@ -36,12 +36,12 @@ ciris-edge = { git = "https://github.com/CIRISAI/CIRISEdge", tag = "v10.1. # CIRISVerify tag persist + edge pin (v5.0.0 — CEG 1.0 / Agent 3.0 substrate # release; MAJOR 4→5 but additive for lens-core) — single verify version. # features mirror edge's keyring pin exactly. -ciris-keyring = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.0", version = "9", features = ["software", "pqc-ml-dsa"] } +ciris-keyring = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.2", version = "9", features = ["software", "pqc-ml-dsa"] } # verify-core for `fedcode::derive_key_id` — the lens seal path must stamp the # DERIVED federation key_id (`derive_key_id(, )`), the id # `receive_and_persist` verifies against, NOT the bare `local_key_id` alias # (CIRISServer#118; same class as the closed CIRISEdge#203). Single verify version. -ciris-verify-core = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.0", version = "9" } +ciris-verify-core = { git = "https://github.com/CIRISAI/CIRISVerify", tag = "v9.0.2", version = "9" } # persist's `sqlite` feature: relay mode persists inbound batches via # `Engine::receive_and_persist` + reads `Engine::sqlite_backend()` for diff --git a/python/ciris_server/__init__.py b/python/ciris_server/__init__.py index c8baae93..76b20ddd 100644 --- a/python/ciris_server/__init__.py +++ b/python/ciris_server/__init__.py @@ -50,4 +50,22 @@ except Exception: # pragma: no cover pass -__all__ = ["main", "import_traces", "__version__"] + +def verify_ffi_path() -> str: + """Absolute path to the shared object carrying the verify FFI symbols. + + ciris-server folds ``ciris-verify-ffi`` (CIRISServer#232) directly into the + compiled ``ciris_server._native`` extension, so the ~84 ``ciris_verify_*`` C + symbols live in *this* wheel's ``.so`` — there is no separate + ``libciris_verify_ffi.so``. The agent's ``ffi_bindings`` ctypes loader points + at the path returned here instead of a standalone ``ciris-verify`` wheel, so + jcs_canonicalize / attestation / self_enc / hybrid_kex / key_grant run against + the SAME verify the substrate uses — version-skew is impossible by construction + (CIRISAgent#917). Returns the ``_native`` extension's own file path. + """ + from . import _native # the compiled extension carrying the folded FFI + + return _native.__file__ + + +__all__ = ["main", "import_traces", "verify_ffi_path", "__version__"] diff --git a/src/compose.rs b/src/compose.rs index bee9c742..4689a968 100644 --- a/src/compose.rs +++ b/src/compose.rs @@ -415,6 +415,18 @@ pub async fn serve_with_adapter(cfg: ServerConfig, adapter: Arc) -> // envelopes. Runs in BOTH the standalone node and the fold (edge is shared). prime_trusted_peers(&engine, &edge).await; + // ── Canonical bootstrap boot prime (CIRISServer#238) ────────────────────── + // `prime_trusted_peers` primes from Rooted `transport_destination` ROWS — but + // the baked canonical seed carries only a KeyRecord (+ an IP dial hint), no + // such row, so the canonical was NEVER boot-primed and rooted only via its + // slow/unreliable announce (~130-200s, or never). An explicit-hash peer needs + // just `(dest_hash, ed25519)`, BOTH deterministically derivable from the fed + // Ed25519 pubkey the seed already carries: `dest_hash = + // reticulum_destination_for_pubkey(fed_ed25519) = sha256(fed_ed25519)[..16]`, + // and the link signing key IS that same fed Ed25519 (transport and federation + // share the Ed25519 signing half; the v10.1.0 split was in the unused X25519). + prime_canonical_bootstrap_peers(&engine, &edge).await; + // ── Holonomic-tier swarm runtime (CIRISServer#11) ───────────────────────── // The publisher advertises the fountain content THIS node holds as signed // FountainHoldingClaim envelopes to the consent cohort; the converger acts @@ -1348,6 +1360,101 @@ async fn prime_trusted_peers(engine: &Engine, edge: &Edge) { ); } +/// Boot-prime the canonical bootstrap peer(s) as explicit-hash Reticulum +/// destinations, derived purely from the baked KeyRecord — CIRISServer#238. +/// +/// The canonical seed carries a `KeyRecord` (with `pubkey_ed25519_base64`) and an +/// IP dial hint, but NO `transport_destination` row, so [`prime_trusted_peers`] +/// (which primes from Rooted rows) never touches it and the canonical roots only +/// via its slow announce. But an explicit-hash peer (edge v7.0.0) needs only +/// `(dest_hash, ed25519)`, and both come from the fed Ed25519 pubkey: +/// - `dest_hash = reticulum_destination_for_pubkey(fed_ed25519) = sha256(fed)[..16]` +/// - `signing_key = fed_ed25519` — transport and federation share the Ed25519 +/// signing half (the split is in the X25519 half, which priming doesn't use). +/// +/// So we look up the canonical's KeyRecord, derive both, and inject a Rooted peer. +/// Deterministic, no announce dependency, no seed change. Runs in BOTH the +/// standalone node and the agent fold. +async fn prime_canonical_bootstrap_peers(engine: &Engine, edge: &Edge) { + use base64::Engine as _; + let Some(transport) = edge.reticulum_transport() else { + tracing::debug!("canonical prime: no Reticulum transport on this build — skipping"); + return; + }; + let hints = match engine.canonical_bootstrap_hints().await { + Ok(h) => h, + Err(e) => { + tracing::warn!(error = %e, "canonical prime: canonical_bootstrap_hints failed — skipping"); + return; + } + }; + let canonical_key_ids = crate::federation_delivery::distinct_canonical_key_ids(&hints); + let mut primed = 0usize; + for key_id in &canonical_key_ids { + // NB: on the canonical node ITSELF this primes its own key_id. That is + // benign (a self-entry in the peers map is never a delivery target) and is + // the same behaviour `prime_trusted_peers` already has — so we don't special + // -case it rather than thread the node's own key_id down here for nothing. + let rec = match engine + .federation_directory() + .lookup_public_key(key_id) + .await + { + Ok(Some(r)) => r, + Ok(None) => { + tracing::warn!( + canonical = %key_id, + "canonical prime: no KeyRecord for canonical key_id — cannot derive explicit-hash binding, will fall back to announce" + ); + continue; + } + Err(e) => { + tracing::warn!(canonical = %key_id, error = %e, "canonical prime: lookup_public_key failed — skip"); + continue; + } + }; + let ed_bytes = match base64::engine::general_purpose::STANDARD + .decode(rec.pubkey_ed25519_base64.as_bytes()) + { + Ok(b) if b.len() == 32 => b, + Ok(b) => { + tracing::warn!( + canonical = %key_id, + len = b.len(), + "canonical prime: pubkey_ed25519_base64 is not 32 bytes — skip" + ); + continue; + } + Err(e) => { + tracing::warn!(canonical = %key_id, error = %e, "canonical prime: pubkey_ed25519_base64 not base64 — skip"); + continue; + } + }; + let mut fed_ed = [0u8; 32]; + fed_ed.copy_from_slice(&ed_bytes); + let dest_hash = + ciris_edge::transport::addressing::reticulum_destination_for_pubkey(&fed_ed); + let before = transport.knows_peer(key_id).await; + transport + .inject_rooted_peer_for_test(key_id, dest_hash, fed_ed) + .await; + let after = transport.knows_peer(key_id).await; + primed += 1; + tracing::info!( + canonical = %key_id, + dest_hash = %hex::encode(dest_hash), + knows_peer_before = before, + knows_peer_after = after, + "canonical boot prime: rooted {key_id} from baked KeyRecord (explicit-hash, no announce)" + ); + } + tracing::info!( + canonical_peers = canonical_key_ids.len(), + primed, + "canonical boot prime complete — canonical(s) reachable-by-key_id at boot with no announce dependency" + ); +} + /// `GET /v1/identity` → the cached identity-aggregate JSON (stable for the /// node's lifetime), merged onto the read-API listener. fn identity_router(identity_json: String) -> axum::Router { diff --git a/src/lib.rs b/src/lib.rs index 916cb800..0e118cd9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -619,6 +619,23 @@ pub fn parse_config_value(raw: &str) -> ConfigValue { mod python { use pyo3::prelude::*; + // ── Verify-FFI keep-alive (CIRISServer#232 / CIRISVerify#189) ──────────── + // We fold `ciris-verify-ffi` (rlib) into `_native.so` so the agent rides us + // for verify and drops its standalone `ciris-verify` wheel. But NOTHING in + // our Rust code calls the FFI's `#[no_mangle] extern "C"` fns (the agent + // reaches them via `ctypes`/`dlopen` at runtime), so the linker's + // `--gc-sections` would dead-strip all ~84 `ciris_verify_*` symbols out of + // the final cdylib — per-platform-silently. Referencing the crate's + // `ciris_verify_ffi_link_anchor()` from a `#[used]` static transitively pins + // every FFI object file; the anchor takes the address of every export, which + // the compiler cannot resolve without keeping the symbol. `verify_ffi_path()` + // (python/ciris_server/__init__.py) resolves `_native`'s own path for the + // agent's ctypes loader. A cross-platform `nm`/`dumpbin` CI smoke asserts the + // surface is actually present in the built artifact (never trust cargo-green). + #[used] + static _KEEP_VERIFY_FFI: extern "C" fn() -> usize = + ciris_verify_ffi::ciris_verify_ffi_link_anchor; + fn rt_block_on>>(fut: F) -> PyResult<()> { // ONE multi-thread runtime; the node spawns onto it (never a second // runtime around the Engine — the persist dual-runtime-deadlock rule). @@ -828,6 +845,10 @@ mod python { /// unchanged at the import sites; only the .so's in-wheel location moved. #[pymodule] fn _native(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + // Belt-and-suspenders alongside the `#[used]` static: a live reference to + // the verify-FFI anchor at init, so the fold survives even a linker that + // ignores `#[used]` on statics (CIRISServer#232). Cheap XOR-fold; discarded. + let _ = std::hint::black_box(ciris_verify_ffi::ciris_verify_ffi_link_anchor()); m.add_function(wrap_pyfunction!(py_main, m)?)?; m.add_function(wrap_pyfunction!(py_import_traces, m)?)?; m.add_function(wrap_pyfunction!(py_serve_with_python_adapter, m)?)?; diff --git a/tests/release_gates/support.rs b/tests/release_gates/support.rs index 69a7410f..e7348963 100644 --- a/tests/release_gates/support.rs +++ b/tests/release_gates/support.rs @@ -34,9 +34,9 @@ use std::path::PathBuf; // 0.5.80: the coordinated edge v9.0.0 + persist v13.0.0 lockstep (CC 1.0 RC1) — // KeyRecord.consent_role, the accord-conferred `canonical` identity_type role // (CIRISPersist#372), single-owner `owner_of` (#162), verify unchanged v8.7.0. -pub const TARGET_VERIFY: &str = "v9.0.0"; -pub const TARGET_PERSIST: &str = "v15.1.0"; -pub const TARGET_EDGE: &str = "v10.1.0"; +pub const TARGET_VERIFY: &str = "v9.0.2"; +pub const TARGET_PERSIST: &str = "v15.1.2"; +pub const TARGET_EDGE: &str = "v10.1.2"; /// Stage 6/7: the persist MAJOR family that bakes the canonical genesis seed. /// (Name is historical — the seed-bake family moved v10 → v12 → **v13**: the v12.0 /// genesis-mesh rooting anchor persisted, v13.0.0 adds the accord-conferred