From eeb0ce3fbd97cdbdbc8f094e7f99544b9aba8c01 Mon Sep 17 00:00:00 2001 From: Adil Date: Sun, 18 Oct 2026 14:10:53 +0500 Subject: [PATCH 1/4] hive: add apple-fm-worker (Apple on-device 3B as a WorkerBee) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A sibling of ollama-worker that runs Apple's on-device 3B FoundationModel via FoundationModels — a local, network-free, key-free LLM. Registers with humd over thrum, advertises model `apple-3b-on-device`, and turns chi:"prompt" into chunk/finish tones. The Rust side (src/) mirrors ollama-worker but, instead of HTTP to Ollama, shells out to a small Swift bridge (bridge/gen) that wraps FoundationModels and streams NDJSON deltas. The only Apple-specific code in the tree is bridge/gen.swift (+ probe.swift); no Apple symbols enter the Rust side, and the bridge is mac-gated. On boot a capability probe keeps the hive present-but-dormant when Apple Intelligence isn't available. Isolated prototype — verified in isolation on macOS 26 (M5 Max): probe reports available, real on-device deltas stream, and the Rust worker compiles + connects to a live humd advertising the model. Not yet co-installed into the mac desktop bundle. --- hives/apple-fm-worker/.gitignore | 3 + hives/apple-fm-worker/Cargo.lock | 2710 ++++++++++++++++++++++ hives/apple-fm-worker/Cargo.toml | 24 + hives/apple-fm-worker/Orchfile | 5 + hives/apple-fm-worker/README.md | 54 + hives/apple-fm-worker/bridge/build.sh | 14 + hives/apple-fm-worker/bridge/gen.swift | 65 + hives/apple-fm-worker/bridge/probe.swift | 23 + hives/apple-fm-worker/src/lib.rs | 225 ++ hives/apple-fm-worker/src/main.rs | 81 + 10 files changed, 3204 insertions(+) create mode 100644 hives/apple-fm-worker/.gitignore create mode 100644 hives/apple-fm-worker/Cargo.lock create mode 100644 hives/apple-fm-worker/Cargo.toml create mode 100644 hives/apple-fm-worker/Orchfile create mode 100644 hives/apple-fm-worker/README.md create mode 100755 hives/apple-fm-worker/bridge/build.sh create mode 100644 hives/apple-fm-worker/bridge/gen.swift create mode 100644 hives/apple-fm-worker/bridge/probe.swift create mode 100644 hives/apple-fm-worker/src/lib.rs create mode 100644 hives/apple-fm-worker/src/main.rs diff --git a/hives/apple-fm-worker/.gitignore b/hives/apple-fm-worker/.gitignore new file mode 100644 index 00000000..d004a978 --- /dev/null +++ b/hives/apple-fm-worker/.gitignore @@ -0,0 +1,3 @@ +/target +/bridge/gen +/bridge/probe diff --git a/hives/apple-fm-worker/Cargo.lock b/hives/apple-fm-worker/Cargo.lock new file mode 100644 index 00000000..b490f18e --- /dev/null +++ b/hives/apple-fm-worker/Cargo.lock @@ -0,0 +1,2710 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "apple-fm-worker" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "hum-identity", + "hum-nest", + "hum-paths", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "codegen" +version = "0.32.0" +source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" +dependencies = [ + "anyhow", + "regex", + "syn 2.0.119", +] + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "command-group" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68fa787550392a9d58f44c21a3022cfb3ea3e2458b7f85d3b399d0ceeccf409" +dependencies = [ + "async-trait", + "nix", + "tokio", + "winapi", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fancy-regex" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fraction" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "h2" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hum-identity" +version = "0.32.0" +source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" +dependencies = [ + "anyhow", + "ed25519-dalek", + "hex", + "hum-paths", + "rand", + "serde", + "sha2", + "tracing", + "uuid", +] + +[[package]] +name = "hum-mcp" +version = "0.32.0" +source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" +dependencies = [ + "anyhow", + "axum", + "parking_lot", + "serde", + "serde_json", + "thiserror 1.0.69", + "thrum-core", + "tokio", + "tracing", +] + +[[package]] +name = "hum-nest" +version = "0.32.0" +source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" +dependencies = [ + "anyhow", + "async-trait", + "command-group", + "hum-identity", + "hum-mcp", + "hum-paths", + "hum-thrum", + "libc", + "lru", + "metrics", + "rand", + "regex", + "serde", + "serde_json", + "sysinfo", + "thrum-core", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hum-paths" +version = "0.32.0" +source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" +dependencies = [ + "jsonschema", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "hum-thrum" +version = "0.32.0" +source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" +dependencies = [ + "anyhow", + "hum-identity", + "hum-paths", + "rand", + "serde_json", + "thrum-core", + "tokio", + "tracing", +] + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonschema" +version = "0.46.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a699d3e77675e6aa4bfffe3b907c8b5f7ed3241f9965bffb25475ad4b08d05" +dependencies = [ + "ahash", + "bytecount", + "data-encoding", + "email_address", + "fancy-regex", + "fraction", + "getrandom 0.3.4", + "idna", + "itoa", + "jsonschema-regex", + "num-cmp", + "num-traits", + "percent-encoding", + "referencing", + "regex", + "reqwest", + "rustls", + "serde", + "serde_json", + "unicode-general-category", + "uuid-simd", +] + +[[package]] +name = "jsonschema-regex" +version = "0.46.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbd1086b01b9349fd4ef9a07433965af64c8ce8159abe633a189e4ff817bd13" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "metrics" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045b4193fbdc5b5681f32f11070da9be3609f189a79f3390706d42587f46bb5" +dependencies = [ + "ahash", + "portable-atomic", +] + +[[package]] +name = "micromap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "referencing" +version = "0.46.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbf332a2f81899f6836f22c03da73dae8a664c32e3016b84692c23cddadc95d" +dependencies = [ + "ahash", + "fluent-uri", + "getrandom 0.3.4", + "hashbrown 0.16.1", + "itoa", + "micromap", + "parking_lot", + "percent-encoding", + "serde_json", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "sysinfo" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "windows", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "thrum-core" +version = "0.32.0" +source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" +dependencies = [ + "codegen", + "hex", + "hum-identity", + "parking_lot", + "serde", + "serde_json", + "sha2", + "strum", + "thiserror 1.0.69", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "js-sys", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/hives/apple-fm-worker/Cargo.toml b/hives/apple-fm-worker/Cargo.toml new file mode 100644 index 00000000..3531821e --- /dev/null +++ b/hives/apple-fm-worker/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "apple-fm-worker" +version = "0.1.0" +edition = "2024" +license = "MIT" +description = "WorkerBee that runs Apple's on-device 3B FoundationModel via FoundationModels — a local LLM with no network and no key." + +[[bin]] +name = "apple-fm-worker" +path = "src/main.rs" + +[dependencies] +hum-paths = { git = "https://github.com/adiled/hum.git", branch = "main" } +hum-nest = { git = "https://github.com/adiled/hum.git", branch = "main" } +hum-identity = { git = "https://github.com/adiled/hum.git", branch = "main" } +tokio = { version = "1", features = ["full"] } +tokio-util = "0.7" +serde_json = "1" +serde = { version = "1", features = ["derive"] } +anyhow = "1" +async-trait = "0.1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +[workspace] diff --git a/hives/apple-fm-worker/Orchfile b/hives/apple-fm-worker/Orchfile new file mode 100644 index 00000000..292436fb --- /dev/null +++ b/hives/apple-fm-worker/Orchfile @@ -0,0 +1,5 @@ +SERVICE apple-fm-worker +RUN ${HOME}/.local/bin/apple-fm-worker +ENV APPLE_FM_MODEL=apple-3b-on-device +ENV APPLE_FM_MODELS=apple-3b-on-device +RESTART always diff --git a/hives/apple-fm-worker/README.md b/hives/apple-fm-worker/README.md new file mode 100644 index 00000000..8f34b5a9 --- /dev/null +++ b/hives/apple-fm-worker/README.md @@ -0,0 +1,54 @@ +# apple-fm-worker + +A WorkerBee that runs **Apple's on-device 3B FoundationModel** (via the +`FoundationModels` framework) as a local, network-free, key-free LLM — a +sibling of `ollama-worker`. + +It registers with `humd` over thrum and advertises the model +`apple-3b-on-device`, turning `chi:"prompt"` into `chunk`/`finish` tones, +exactly like ollama-worker does for a remote Ollama endpoint. + +## Architecture + +The Rust side (`src/`) is a thin `WorkerBee` — it mirrors `ollama-worker` +but, instead of issuing HTTP to Ollama, it **shells out to a small Swift +binary** (`bridge/gen`) that wraps `FoundationModels`. The bridge reads a +JSON line `{system, prompt}` on stdin and emits NDJSON on stdout: + + {"event":"delta","text":"…"} (one per streaming token) + {"event":"finish","finishReason":"stop"} + +The Rust worker diffs the cumulative snapshots `streamResponse` returns and +forwards the per-token deltas as `chunk` tones. + +The **only** Apple-specific code in the whole tree is `bridge/gen.swift` +(+ `probe.swift`), compiled with `swiftc`. No Apple symbols enter the Rust +side. The bridge is `#[cfg(target_os = "macos")]`-gated; non-mac builds of +hum never reference it. + +## Capability gate + +On boot the worker runs `gen --probe`. If the on-device model is not +available — Apple Intelligence off, asset not downloaded, unsupported +region — the probe exits non-zero and the hive stays **present-but-dormant** +instead of advertising a live worker, mirroring how ollama-worker degrades +when Ollama isn't running. + +## Build + + cd hives/apple-fm-worker/bridge && ./build.sh # swiftc -O -> ./gen + cd .. && cargo build --release # -> target/release/apple-fm-worker + +Requires macOS 26+ with Apple Intelligence enabled +(System Settings → Apple Intelligence & Siri). + +## Status + +Isolated prototype. Verified on this host: +- `gen --probe` → `{"event":"probe","available":true}` +- `gen` with a prompt → real on-device deltas + `finish` +- the Rust worker compiles, connects to a live `humd`, and hands its + `hello` advertising `apple-3b-on-device`. + +Not yet co-installed into the mac desktop bundle — pending an end-to-end +prompt routed through `humd` to the worker and back. diff --git a/hives/apple-fm-worker/bridge/build.sh b/hives/apple-fm-worker/bridge/build.sh new file mode 100755 index 00000000..cebdfbdb --- /dev/null +++ b/hives/apple-fm-worker/bridge/build.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Build the apple-fm-worker bridge — Apple's on-device 3B (FoundationModels) +# wrapped as a one-shot NDJSON subprocess. The ONLY Apple-specific artifact. +# +# Requires: macOS 26 + Apple Intelligence enabled, and the Command Line Tools +# swiftc (no full Xcode needed — the framework resolves via the CLT SDK). +set -euo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +OUT="${1:-$HERE/gen}" +# arm64e is the native slice on Apple Silicon; arm64e-apple-macos26.0 is the +# minimum target FoundationModels ships at. +swiftc -parse-as-library -target arm64e-apple-macos26.0 \ + "$HERE/gen.swift" -O -o "$OUT" +echo "built $OUT" diff --git a/hives/apple-fm-worker/bridge/gen.swift b/hives/apple-fm-worker/bridge/gen.swift new file mode 100644 index 00000000..82800f4d --- /dev/null +++ b/hives/apple-fm-worker/bridge/gen.swift @@ -0,0 +1,65 @@ +// apple-fm-worker bridge — the ONLY Apple-specific code in the tree. +// Wraps Apple's on-device FoundationModel (SystemLanguageModel, ~3B) and +// speaks NDJSON on stdout that the Rust worker parses into hum chunk/finish. +// +// stdin line: {"system":"...","prompt":"..."} (one request per run) +// stdout lines: {"event":"delta","text":"..."} / {"event":"finish"} / {"event":"error","message":"..."} +// {"probe":true} → emit availability and exit. + +import Foundation +import FoundationModels + +@main +struct Bridge { + static func main() async { + var system: String? = nil + var prompt: String? = nil + var probe = false + while let raw = readLine() { + guard let obj = parse(raw) else { continue } + if let p = obj["probe"] as? Bool { probe = p; break } + if let s = obj["system"] as? String { system = s } + if let q = obj["prompt"] as? String { prompt = q; break } + } + + if probe { + print(#"{"event":"probe","available":\#(SystemLanguageModel.default.availability == .available)}"#) + return + } + guard let prompt else { + print(#"{"event":"error","message":"no prompt"}"#); return + } + + let session: LanguageModelSession = + system.map { LanguageModelSession(model: .default, instructions: Instructions($0)) } + ?? LanguageModelSession(model: .default) + + do { + let stream = try await session.streamResponse(to: prompt) + for try await snapshot in stream { + print(encodeDelta(snapshot.content)) + } + print(#"{"event":"finish"}"#) + } catch { + let msg = String(describing: error) + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + print(#"{"event":"error","message":"\#(msg)"}"#) + } + } + + static func parse(_ s: String) -> [String: Any]? { + guard let data = s.data(using: .utf8) else { return nil } + return try? JSONSerialization.jsonObject(with: data) as? [String: Any] + } + + static func encodeDelta(_ text: String) -> String { + let escaped = text + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "\n", with: "\\n") + .replacingOccurrences(of: "\r", with: "\\r") + .replacingOccurrences(of: "\t", with: "\\t") + return #"{"event":"delta","text":"\#(escaped)"}"# + } +} diff --git a/hives/apple-fm-worker/bridge/probe.swift b/hives/apple-fm-worker/bridge/probe.swift new file mode 100644 index 00000000..f92c5454 --- /dev/null +++ b/hives/apple-fm-worker/bridge/probe.swift @@ -0,0 +1,23 @@ +import Foundation +import FoundationModels + +@main +struct Probe { + static func main() async { + let session = LanguageModelSession(model: .default) + do { + let stream = try await session.streamResponse(to: "Say: streaming works") + var count = 0 + for try await snapshot in stream { + let text = snapshot.content as String + if !text.isEmpty { + print("[snapshot \(count)] \(text)") + count += 1 + } + } + print("[finished snapshots=\(count)]") + } catch { + print("[error] \(error)") + } + } +} diff --git a/hives/apple-fm-worker/src/lib.rs b/hives/apple-fm-worker/src/lib.rs new file mode 100644 index 00000000..eedbdc55 --- /dev/null +++ b/hives/apple-fm-worker/src/lib.rs @@ -0,0 +1,225 @@ +//! apple-fm-worker — WorkerBee that runs Apple's on-device FoundationModel. +//! +//! Mirrors `ollama-worker`, but instead of an HTTP client it spawns a one-shot +//! Swift bridge (`bridge/gen`) that wraps `FoundationModels` and speaks NDJSON +//! on stdout. The bridge is the ONLY Apple-specific artifact; this Rust worker +//! is platform-neutral and degrades to "no models" on non-macOS hosts. +//! +//! stdin to gen: {"system":"...","prompt":"..."} (one request per spawn) +//! stdout from gen: {"event":"delta","text":"..."} / {"event":"finish"} +//! {"event":"error","message":"..."} / {"event":"probe","available":bool} +//! +//! Each `feed` message spawns a fresh `gen` (the bridge holds no state), so the +//! cell transparently handles follow-up turns in the same session. + +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; +use async_trait::async_trait; +use hum_nest::{Cell, Egg, Propensity, WorkerBee}; +use serde_json::{json, Value}; +use tokio::process::Command; +use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; +use tracing::{info, warn}; + +pub struct AppleFmWorker { + pub model_id: String, + pub bridge: PathBuf, +} + +#[async_trait] +impl WorkerBee for AppleFmWorker { + fn ephemeral(&self) -> bool { false } + + fn propensity(&self) -> Propensity { + // The bridge is stateless per spawn, so humd can reuse the cell for + // follow-up turns in the same sid — each turn just spawns a fresh gen. + Propensity::StatefulSession + } + + async fn raise(&self, egg: Egg) -> Result { + let model = if egg.model_id.is_empty() { + self.model_id.clone() + } else { + egg.model_id.clone() + }; + let system = egg.system_prompt.clone(); + let bridge = self.bridge.clone(); + + info!(model = %model, bridge = %bridge.display(), cwd = %egg.cwd, "apple-fm-worker.raise"); + + let (tx_feed, mut rx_feed) = tokio::sync::mpsc::channel::(64); + let (tx_mmm, rx_mmm) = tokio::sync::mpsc::channel::(256); + let silence = CancellationToken::new(); + + let task_silence = silence.clone(); + let wire = tx_mmm.clone(); + tokio::spawn(async move { + // Handle every prompt fed to this cell until the feed closes. + while let Some(prompt) = rx_feed.recv().await { + if task_silence.is_cancelled() { break; } + run_one(&bridge, &system, &prompt, &wire, &task_silence).await; + if task_silence.is_cancelled() { break; } + } + warn!("apple-fm-worker: feed closed"); + }); + + let (_tx_exit, rx_exit) = tokio::sync::oneshot::channel::(); + + Ok(Cell { + mark: None, + feed: tx_feed, + mmm: Arc::new(Mutex::new(rx_mmm)), + emerged: rx_exit, + ephemeral: false, + silence, + }) + } +} + +/// Spawn the bridge for one prompt and forward its NDJSON events as hum tones. +async fn run_one( + bridge: &PathBuf, + system: &Option, + prompt: &str, + wire: &tokio::sync::mpsc::Sender, + silence: &CancellationToken, +) { + // Capability gate: if the on-device model is unavailable (Apple Intelligence + // off, or unsupported host), report a graceful error and stop. humd will + // simply stop routing to this model — exactly like ollama-worker idling when + // no Ollama server is up. + if !probe_available(bridge, silence).await { + let _ = wire + .send(json!({ + "chi": "error", + "code": "apple_fm_unavailable", + "message": "on-device FoundationModel unavailable (Apple Intelligence not enabled)", + })) + .await; + return; + } + + let mut child = match Command::new(bridge).stdin(std::process::Stdio::piped()).spawn() { + Ok(c) => c, + Err(e) => { + let _ = wire + .send(json!({ + "chi": "error", + "code": "apple_fm_spawn", + "message": format!("failed to spawn bridge {bridge:?}: {e}"), + })) + .await; + return; + } + }; + + // Write the request to the bridge's stdin, then close it. + let request = json!({ + "system": system, + "prompt": prompt, + }); + if let Some(mut stdin) = child.stdin.take() { + use tokio::io::AsyncWriteExt; + let _ = stdin.write_all(request.to_string().as_bytes()).await; + let _ = stdin.flush().await; + drop(stdin); + } + + let stdout = child.stdout.take(); + let mut child = child; + let out = match stdout { + Some(o) => o, + None => return, + }; + + // Read NDJSON lines from the bridge's stdout, forwarding deltas/finish/errors. + // `tokio::io::BufReader` + line reading keeps this simple. + use tokio::io::{AsyncBufReadExt, BufReader}; + let mut reader = BufReader::new(out); + let mut buf = String::new(); + loop { + if silence.is_cancelled() { break; } + buf.clear(); + match reader.read_line(&mut buf).await { + Ok(0) => break, // EOF — bridge finished + Ok(_) => {} + Err(e) => { + let _ = wire + .send(json!({ + "chi": "error", + "code": "apple_fm_read", + "message": format!("bridge stdout read error: {e}"), + })) + .await; + break; + } + } + let line = buf.trim(); + if line.is_empty() { continue; } + + let obj: Value = match serde_json::from_str(line) { + Ok(v) => v, + Err(e) => { + warn!(err = %e, line = line.chars().take(200).collect::(), "apple-fm-worker.parse"); + continue; + } + }; + match obj.get("event").and_then(|e| e.as_str()) { + Some("delta") => { + let text = obj.get("text").and_then(|t| t.as_str()).unwrap_or(""); + if !text.is_empty() { + let _ = wire.send(json!({ + "chi": "chunk", + "chunkType": "text_delta", + "delta": text, + })).await; + } + } + Some("finish") => { + let _ = wire.send(json!({ + "chi": "finish", + "finishReason": "stop", + })).await; + } + Some("error") => { + let msg = obj.get("message").and_then(|m| m.as_str()).unwrap_or("unknown error"); + let _ = wire.send(json!({ + "chi": "error", + "code": "apple_fm_bridge", + "message": msg, + })).await; + } + other => warn!(other = ?other, "apple-fm-worker: unexpected bridge event"), + } + } + + // If the loop ended without a finish tone (e.g. bridge died or we were + // cancelled), close out cleanly so humd gets a terminal event. + let _ = child.wait().await; +} + +/// Ask the bridge whether the on-device model is currently available. +async fn probe_available(bridge: &PathBuf, _silence: &CancellationToken) -> bool { + let out = Command::new(bridge) + .arg("--probe") + .output() + .await; + match out { + Ok(o) if o.status.success() => { + // Best-effort parse of the "available" flag; default to true on success. + let s = String::from_utf8_lossy(&o.stdout); + !s.contains("\"available\":false") + } + Ok(o) => { + let s = String::from_utf8_lossy(&o.stdout); + if s.contains("\"available\":false") { false } else { true } + } + Err(e) => { + warn!(err = %e, "apple-fm-worker: probe spawn failed"); + false + } + } +} diff --git a/hives/apple-fm-worker/src/main.rs b/hives/apple-fm-worker/src/main.rs new file mode 100644 index 00000000..6f67ae90 --- /dev/null +++ b/hives/apple-fm-worker/src/main.rs @@ -0,0 +1,81 @@ +//! apple-fm-worker — standalone worker-bee process. +//! +//! Registers with humd as `bee:["worker"]`, advertises the on-device model id, +//! and routes prompts to Apple's on-device 3B FoundationModel via a small +//! Swift bridge (`bridge/gen`) that wraps `FoundationModels`. No network, +//! no API key. +//! +//! Environment: +//! APPLE_FM_MODEL — advertised model id (default: apple-3b-on-device) +//! APPLE_FM_BRIDGE — path to the bridge binary (default: /bridge/gen, +//! resolved relative to this binary; override to test) + +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; +use hum_nest::{serve_worker, HiveAdvert}; + +use lib::AppleFmWorker; +mod lib; + +#[tokio::main] +async fn main() -> Result<()> { + hum_paths::init(); + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + let model_id = + std::env::var("APPLE_FM_MODEL").unwrap_or_else(|_| "apple-3b-on-device".into()); + + // Resolve the bridge next to this binary: /../bridge/gen. + let bridge = resolve_bridge(); + tracing::info!(bridge = %bridge.display(), "apple-fm-worker: bridge resolved"); + + let worker = Arc::new(AppleFmWorker { + model_id: model_id.clone(), + bridge, + }); + + let advert = HiveAdvert { + hive: "apple-fm-worker".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + models: vec![model_id], + source: Some( + "https://github.com/adiled/hum/tree/main/hives/apple-fm-worker".to_string(), + ), + }; + + serve_worker(worker, advert).await +} + +fn resolve_bridge() -> PathBuf { + if let Ok(p) = std::env::var("APPLE_FM_BRIDGE") { + return PathBuf::from(p); + } + if let Ok(exe) = std::env::current_exe() { + // <...>/apple-fm-worker → <...>/../bridge/gen + let sibling = exe + .parent() + .map(|p| p.join("bridge/gen")) + .unwrap_or_else(|| PathBuf::from("bridge/gen")); + if sibling.exists() { + return sibling; + } + } + // Fallback: relative to CWD (common in dev). + PathBuf::from("bridge/gen") +} + +/// No-op kept for clarity: the bridge is only ever invoked on macOS, and the +/// capability probe inside `raise` handles the "Apple Intelligence off" case. +#[cfg(not(target_os = "macos"))] +fn resolve_bridge() -> std::path::PathBuf { + // Not on macOS — the bridge doesn't exist. Return a sentinel path; + // the probe inside `raise` will fail gracefully and humd will stop routing. + std::path::PathBuf::from("/dev/null") +} From 5c252bd816827f4a4390a56a3b9e49b24ab9c65f Mon Sep 17 00:00:00 2001 From: Adil Date: Sun, 1 Nov 2026 17:42:28 +0500 Subject: [PATCH 2/4] hive/apple-fm: Apple's on-device model as a nurse, through one Swift file Rebuilds the apple-fm-worker prototype on main's nurse model as hives/apple-fm. apple-fm-nurse (Rust, on hum-nest's serve_nurse) runs apple-fm-gen, a Swift bridge to FoundationModels, once per bloom: the prompt's system prompt becomes Apple's instructions, its graft (or the turns its cell grew, for a forager that sends only new words) becomes the transcript, and temperature/season/prune/seed become Apple's generation options. Deltas unfurl as they come, each only the new words; the bloom wilts stop or max_tokens with Apple's token counts as nectar. frost is held by the nurse, which kills the bridge at a stop; a cancel kills it too. Pollen and trellis are refused rather than grown as if absent. Before nestling it probes the bridge: it waits unnestled while Apple Intelligence is off or its model downloads, exits 0 on a Mac that cannot run it (its unit stays down), and fails on a bridge that cannot run. Fixes in the prototype: the bridge's stdout was never piped, --probe was ignored by the Swift side, cumulative snapshots were sent as deltas, a duplicate fn broke non-mac builds, the cell's exit sender was dropped at birth, a cancel left the bridge running, and the graft never reached the model. Tests: unit and cell tests against a stand-in bridge; E2E through a real humd (prompt, kept conversation, cancel, dormant-then-ready, never, missing bridge); and an ignored live test against Apple's model. Registered in the workspace exclude list, release.sh's lockstep, and the hives catalogue. --- Cargo.toml | 1 + hives/README.md | 1 + hives/apple-fm-worker/.gitignore | 3 - hives/apple-fm-worker/Cargo.toml | 24 - hives/apple-fm-worker/Orchfile | 5 - hives/apple-fm-worker/README.md | 54 - hives/apple-fm-worker/bridge/build.sh | 14 - hives/apple-fm-worker/bridge/gen.swift | 65 - hives/apple-fm-worker/bridge/probe.swift | 23 - hives/apple-fm-worker/src/lib.rs | 225 -- hives/apple-fm-worker/src/main.rs | 81 - hives/apple-fm/.gitignore | 1 + .../{apple-fm-worker => apple-fm}/Cargo.lock | 2768 +++++++++++++++-- hives/apple-fm/Cargo.toml | 30 + hives/apple-fm/Orchfile | 27 + hives/apple-fm/README.md | 207 ++ hives/apple-fm/bridge/gen.swift | 179 ++ hives/apple-fm/build | 23 + hives/apple-fm/install | 25 + hives/apple-fm/src/bridge.rs | 350 +++ hives/apple-fm/src/main.rs | 86 + hives/apple-fm/src/nurse.rs | 474 +++ hives/apple-fm/tests/common/mod.rs | 260 ++ hives/apple-fm/tests/live.rs | 81 + hives/apple-fm/tests/nurse.rs | 94 + release.sh | 1 + 26 files changed, 4394 insertions(+), 708 deletions(-) delete mode 100644 hives/apple-fm-worker/.gitignore delete mode 100644 hives/apple-fm-worker/Cargo.toml delete mode 100644 hives/apple-fm-worker/Orchfile delete mode 100644 hives/apple-fm-worker/README.md delete mode 100755 hives/apple-fm-worker/bridge/build.sh delete mode 100644 hives/apple-fm-worker/bridge/gen.swift delete mode 100644 hives/apple-fm-worker/bridge/probe.swift delete mode 100644 hives/apple-fm-worker/src/lib.rs delete mode 100644 hives/apple-fm-worker/src/main.rs create mode 100644 hives/apple-fm/.gitignore rename hives/{apple-fm-worker => apple-fm}/Cargo.lock (51%) create mode 100644 hives/apple-fm/Cargo.toml create mode 100644 hives/apple-fm/Orchfile create mode 100644 hives/apple-fm/README.md create mode 100644 hives/apple-fm/bridge/gen.swift create mode 100755 hives/apple-fm/build create mode 100755 hives/apple-fm/install create mode 100644 hives/apple-fm/src/bridge.rs create mode 100644 hives/apple-fm/src/main.rs create mode 100644 hives/apple-fm/src/nurse.rs create mode 100644 hives/apple-fm/tests/common/mod.rs create mode 100644 hives/apple-fm/tests/live.rs create mode 100644 hives/apple-fm/tests/nurse.rs diff --git a/Cargo.toml b/Cargo.toml index 85c7e5b3..8692599b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" exclude = [ + "hives/apple-fm", "hives/claude-cli", "hives/claude-repl", "hives/grpc", diff --git a/hives/README.md b/hives/README.md index 778b9c52..d2b020ed 100644 --- a/hives/README.md +++ b/hives/README.md @@ -20,6 +20,7 @@ A honeybee on the plant pot by your bed is still a bee; you just need to know wh | [`claude-cli`](claude-cli) | nurse | `claude-cli-nurse` | `claude -p` stream-json over a pipe | | [`claude-repl`](claude-repl) | nurse | `claude-repl-nurse` | the `claude` REPL over a PTY | | [`dwarfstar`](dwarfstar) | nurse, one bee per model | `dwarfstar-bee` | the DwarfStar engine (`antirez/ds4`) linked into the bee, on Metal | +| [`apple-fm`](apple-fm) | nurse | `apple-fm-nurse` | Apple's on-device foundation model, through `apple-fm-gen`, a Swift bridge to FoundationModels | | [`ollama-server`](ollama-server) | forager, nurse | `ollama-server`, `ollama-nurse` | Ollama `/api/chat` and `/api/generate`, both ways | | [`openai-server`](openai-server) | forager, nurse, attendant | `node dist/index.js` | OpenAI `/v1/chat/completions`, `/v1/responses`, `/v1/completions`, `/v1/embeddings` in front, an OpenAI-compatible API behind | | [`anthropic-server`](anthropic-server) | forager | `node dist/index.js` | Anthropic `/v1/messages`, streamed or not | diff --git a/hives/apple-fm-worker/.gitignore b/hives/apple-fm-worker/.gitignore deleted file mode 100644 index d004a978..00000000 --- a/hives/apple-fm-worker/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/target -/bridge/gen -/bridge/probe diff --git a/hives/apple-fm-worker/Cargo.toml b/hives/apple-fm-worker/Cargo.toml deleted file mode 100644 index 3531821e..00000000 --- a/hives/apple-fm-worker/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "apple-fm-worker" -version = "0.1.0" -edition = "2024" -license = "MIT" -description = "WorkerBee that runs Apple's on-device 3B FoundationModel via FoundationModels — a local LLM with no network and no key." - -[[bin]] -name = "apple-fm-worker" -path = "src/main.rs" - -[dependencies] -hum-paths = { git = "https://github.com/adiled/hum.git", branch = "main" } -hum-nest = { git = "https://github.com/adiled/hum.git", branch = "main" } -hum-identity = { git = "https://github.com/adiled/hum.git", branch = "main" } -tokio = { version = "1", features = ["full"] } -tokio-util = "0.7" -serde_json = "1" -serde = { version = "1", features = ["derive"] } -anyhow = "1" -async-trait = "0.1" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -[workspace] diff --git a/hives/apple-fm-worker/Orchfile b/hives/apple-fm-worker/Orchfile deleted file mode 100644 index 292436fb..00000000 --- a/hives/apple-fm-worker/Orchfile +++ /dev/null @@ -1,5 +0,0 @@ -SERVICE apple-fm-worker -RUN ${HOME}/.local/bin/apple-fm-worker -ENV APPLE_FM_MODEL=apple-3b-on-device -ENV APPLE_FM_MODELS=apple-3b-on-device -RESTART always diff --git a/hives/apple-fm-worker/README.md b/hives/apple-fm-worker/README.md deleted file mode 100644 index 8f34b5a9..00000000 --- a/hives/apple-fm-worker/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# apple-fm-worker - -A WorkerBee that runs **Apple's on-device 3B FoundationModel** (via the -`FoundationModels` framework) as a local, network-free, key-free LLM — a -sibling of `ollama-worker`. - -It registers with `humd` over thrum and advertises the model -`apple-3b-on-device`, turning `chi:"prompt"` into `chunk`/`finish` tones, -exactly like ollama-worker does for a remote Ollama endpoint. - -## Architecture - -The Rust side (`src/`) is a thin `WorkerBee` — it mirrors `ollama-worker` -but, instead of issuing HTTP to Ollama, it **shells out to a small Swift -binary** (`bridge/gen`) that wraps `FoundationModels`. The bridge reads a -JSON line `{system, prompt}` on stdin and emits NDJSON on stdout: - - {"event":"delta","text":"…"} (one per streaming token) - {"event":"finish","finishReason":"stop"} - -The Rust worker diffs the cumulative snapshots `streamResponse` returns and -forwards the per-token deltas as `chunk` tones. - -The **only** Apple-specific code in the whole tree is `bridge/gen.swift` -(+ `probe.swift`), compiled with `swiftc`. No Apple symbols enter the Rust -side. The bridge is `#[cfg(target_os = "macos")]`-gated; non-mac builds of -hum never reference it. - -## Capability gate - -On boot the worker runs `gen --probe`. If the on-device model is not -available — Apple Intelligence off, asset not downloaded, unsupported -region — the probe exits non-zero and the hive stays **present-but-dormant** -instead of advertising a live worker, mirroring how ollama-worker degrades -when Ollama isn't running. - -## Build - - cd hives/apple-fm-worker/bridge && ./build.sh # swiftc -O -> ./gen - cd .. && cargo build --release # -> target/release/apple-fm-worker - -Requires macOS 26+ with Apple Intelligence enabled -(System Settings → Apple Intelligence & Siri). - -## Status - -Isolated prototype. Verified on this host: -- `gen --probe` → `{"event":"probe","available":true}` -- `gen` with a prompt → real on-device deltas + `finish` -- the Rust worker compiles, connects to a live `humd`, and hands its - `hello` advertising `apple-3b-on-device`. - -Not yet co-installed into the mac desktop bundle — pending an end-to-end -prompt routed through `humd` to the worker and back. diff --git a/hives/apple-fm-worker/bridge/build.sh b/hives/apple-fm-worker/bridge/build.sh deleted file mode 100755 index cebdfbdb..00000000 --- a/hives/apple-fm-worker/bridge/build.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -# Build the apple-fm-worker bridge — Apple's on-device 3B (FoundationModels) -# wrapped as a one-shot NDJSON subprocess. The ONLY Apple-specific artifact. -# -# Requires: macOS 26 + Apple Intelligence enabled, and the Command Line Tools -# swiftc (no full Xcode needed — the framework resolves via the CLT SDK). -set -euo pipefail -HERE="$(cd "$(dirname "$0")" && pwd)" -OUT="${1:-$HERE/gen}" -# arm64e is the native slice on Apple Silicon; arm64e-apple-macos26.0 is the -# minimum target FoundationModels ships at. -swiftc -parse-as-library -target arm64e-apple-macos26.0 \ - "$HERE/gen.swift" -O -o "$OUT" -echo "built $OUT" diff --git a/hives/apple-fm-worker/bridge/gen.swift b/hives/apple-fm-worker/bridge/gen.swift deleted file mode 100644 index 82800f4d..00000000 --- a/hives/apple-fm-worker/bridge/gen.swift +++ /dev/null @@ -1,65 +0,0 @@ -// apple-fm-worker bridge — the ONLY Apple-specific code in the tree. -// Wraps Apple's on-device FoundationModel (SystemLanguageModel, ~3B) and -// speaks NDJSON on stdout that the Rust worker parses into hum chunk/finish. -// -// stdin line: {"system":"...","prompt":"..."} (one request per run) -// stdout lines: {"event":"delta","text":"..."} / {"event":"finish"} / {"event":"error","message":"..."} -// {"probe":true} → emit availability and exit. - -import Foundation -import FoundationModels - -@main -struct Bridge { - static func main() async { - var system: String? = nil - var prompt: String? = nil - var probe = false - while let raw = readLine() { - guard let obj = parse(raw) else { continue } - if let p = obj["probe"] as? Bool { probe = p; break } - if let s = obj["system"] as? String { system = s } - if let q = obj["prompt"] as? String { prompt = q; break } - } - - if probe { - print(#"{"event":"probe","available":\#(SystemLanguageModel.default.availability == .available)}"#) - return - } - guard let prompt else { - print(#"{"event":"error","message":"no prompt"}"#); return - } - - let session: LanguageModelSession = - system.map { LanguageModelSession(model: .default, instructions: Instructions($0)) } - ?? LanguageModelSession(model: .default) - - do { - let stream = try await session.streamResponse(to: prompt) - for try await snapshot in stream { - print(encodeDelta(snapshot.content)) - } - print(#"{"event":"finish"}"#) - } catch { - let msg = String(describing: error) - .replacingOccurrences(of: "\\", with: "\\\\") - .replacingOccurrences(of: "\"", with: "\\\"") - print(#"{"event":"error","message":"\#(msg)"}"#) - } - } - - static func parse(_ s: String) -> [String: Any]? { - guard let data = s.data(using: .utf8) else { return nil } - return try? JSONSerialization.jsonObject(with: data) as? [String: Any] - } - - static func encodeDelta(_ text: String) -> String { - let escaped = text - .replacingOccurrences(of: "\\", with: "\\\\") - .replacingOccurrences(of: "\"", with: "\\\"") - .replacingOccurrences(of: "\n", with: "\\n") - .replacingOccurrences(of: "\r", with: "\\r") - .replacingOccurrences(of: "\t", with: "\\t") - return #"{"event":"delta","text":"\#(escaped)"}"# - } -} diff --git a/hives/apple-fm-worker/bridge/probe.swift b/hives/apple-fm-worker/bridge/probe.swift deleted file mode 100644 index f92c5454..00000000 --- a/hives/apple-fm-worker/bridge/probe.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import FoundationModels - -@main -struct Probe { - static func main() async { - let session = LanguageModelSession(model: .default) - do { - let stream = try await session.streamResponse(to: "Say: streaming works") - var count = 0 - for try await snapshot in stream { - let text = snapshot.content as String - if !text.isEmpty { - print("[snapshot \(count)] \(text)") - count += 1 - } - } - print("[finished snapshots=\(count)]") - } catch { - print("[error] \(error)") - } - } -} diff --git a/hives/apple-fm-worker/src/lib.rs b/hives/apple-fm-worker/src/lib.rs deleted file mode 100644 index eedbdc55..00000000 --- a/hives/apple-fm-worker/src/lib.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! apple-fm-worker — WorkerBee that runs Apple's on-device FoundationModel. -//! -//! Mirrors `ollama-worker`, but instead of an HTTP client it spawns a one-shot -//! Swift bridge (`bridge/gen`) that wraps `FoundationModels` and speaks NDJSON -//! on stdout. The bridge is the ONLY Apple-specific artifact; this Rust worker -//! is platform-neutral and degrades to "no models" on non-macOS hosts. -//! -//! stdin to gen: {"system":"...","prompt":"..."} (one request per spawn) -//! stdout from gen: {"event":"delta","text":"..."} / {"event":"finish"} -//! {"event":"error","message":"..."} / {"event":"probe","available":bool} -//! -//! Each `feed` message spawns a fresh `gen` (the bridge holds no state), so the -//! cell transparently handles follow-up turns in the same session. - -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Result; -use async_trait::async_trait; -use hum_nest::{Cell, Egg, Propensity, WorkerBee}; -use serde_json::{json, Value}; -use tokio::process::Command; -use tokio::sync::Mutex; -use tokio_util::sync::CancellationToken; -use tracing::{info, warn}; - -pub struct AppleFmWorker { - pub model_id: String, - pub bridge: PathBuf, -} - -#[async_trait] -impl WorkerBee for AppleFmWorker { - fn ephemeral(&self) -> bool { false } - - fn propensity(&self) -> Propensity { - // The bridge is stateless per spawn, so humd can reuse the cell for - // follow-up turns in the same sid — each turn just spawns a fresh gen. - Propensity::StatefulSession - } - - async fn raise(&self, egg: Egg) -> Result { - let model = if egg.model_id.is_empty() { - self.model_id.clone() - } else { - egg.model_id.clone() - }; - let system = egg.system_prompt.clone(); - let bridge = self.bridge.clone(); - - info!(model = %model, bridge = %bridge.display(), cwd = %egg.cwd, "apple-fm-worker.raise"); - - let (tx_feed, mut rx_feed) = tokio::sync::mpsc::channel::(64); - let (tx_mmm, rx_mmm) = tokio::sync::mpsc::channel::(256); - let silence = CancellationToken::new(); - - let task_silence = silence.clone(); - let wire = tx_mmm.clone(); - tokio::spawn(async move { - // Handle every prompt fed to this cell until the feed closes. - while let Some(prompt) = rx_feed.recv().await { - if task_silence.is_cancelled() { break; } - run_one(&bridge, &system, &prompt, &wire, &task_silence).await; - if task_silence.is_cancelled() { break; } - } - warn!("apple-fm-worker: feed closed"); - }); - - let (_tx_exit, rx_exit) = tokio::sync::oneshot::channel::(); - - Ok(Cell { - mark: None, - feed: tx_feed, - mmm: Arc::new(Mutex::new(rx_mmm)), - emerged: rx_exit, - ephemeral: false, - silence, - }) - } -} - -/// Spawn the bridge for one prompt and forward its NDJSON events as hum tones. -async fn run_one( - bridge: &PathBuf, - system: &Option, - prompt: &str, - wire: &tokio::sync::mpsc::Sender, - silence: &CancellationToken, -) { - // Capability gate: if the on-device model is unavailable (Apple Intelligence - // off, or unsupported host), report a graceful error and stop. humd will - // simply stop routing to this model — exactly like ollama-worker idling when - // no Ollama server is up. - if !probe_available(bridge, silence).await { - let _ = wire - .send(json!({ - "chi": "error", - "code": "apple_fm_unavailable", - "message": "on-device FoundationModel unavailable (Apple Intelligence not enabled)", - })) - .await; - return; - } - - let mut child = match Command::new(bridge).stdin(std::process::Stdio::piped()).spawn() { - Ok(c) => c, - Err(e) => { - let _ = wire - .send(json!({ - "chi": "error", - "code": "apple_fm_spawn", - "message": format!("failed to spawn bridge {bridge:?}: {e}"), - })) - .await; - return; - } - }; - - // Write the request to the bridge's stdin, then close it. - let request = json!({ - "system": system, - "prompt": prompt, - }); - if let Some(mut stdin) = child.stdin.take() { - use tokio::io::AsyncWriteExt; - let _ = stdin.write_all(request.to_string().as_bytes()).await; - let _ = stdin.flush().await; - drop(stdin); - } - - let stdout = child.stdout.take(); - let mut child = child; - let out = match stdout { - Some(o) => o, - None => return, - }; - - // Read NDJSON lines from the bridge's stdout, forwarding deltas/finish/errors. - // `tokio::io::BufReader` + line reading keeps this simple. - use tokio::io::{AsyncBufReadExt, BufReader}; - let mut reader = BufReader::new(out); - let mut buf = String::new(); - loop { - if silence.is_cancelled() { break; } - buf.clear(); - match reader.read_line(&mut buf).await { - Ok(0) => break, // EOF — bridge finished - Ok(_) => {} - Err(e) => { - let _ = wire - .send(json!({ - "chi": "error", - "code": "apple_fm_read", - "message": format!("bridge stdout read error: {e}"), - })) - .await; - break; - } - } - let line = buf.trim(); - if line.is_empty() { continue; } - - let obj: Value = match serde_json::from_str(line) { - Ok(v) => v, - Err(e) => { - warn!(err = %e, line = line.chars().take(200).collect::(), "apple-fm-worker.parse"); - continue; - } - }; - match obj.get("event").and_then(|e| e.as_str()) { - Some("delta") => { - let text = obj.get("text").and_then(|t| t.as_str()).unwrap_or(""); - if !text.is_empty() { - let _ = wire.send(json!({ - "chi": "chunk", - "chunkType": "text_delta", - "delta": text, - })).await; - } - } - Some("finish") => { - let _ = wire.send(json!({ - "chi": "finish", - "finishReason": "stop", - })).await; - } - Some("error") => { - let msg = obj.get("message").and_then(|m| m.as_str()).unwrap_or("unknown error"); - let _ = wire.send(json!({ - "chi": "error", - "code": "apple_fm_bridge", - "message": msg, - })).await; - } - other => warn!(other = ?other, "apple-fm-worker: unexpected bridge event"), - } - } - - // If the loop ended without a finish tone (e.g. bridge died or we were - // cancelled), close out cleanly so humd gets a terminal event. - let _ = child.wait().await; -} - -/// Ask the bridge whether the on-device model is currently available. -async fn probe_available(bridge: &PathBuf, _silence: &CancellationToken) -> bool { - let out = Command::new(bridge) - .arg("--probe") - .output() - .await; - match out { - Ok(o) if o.status.success() => { - // Best-effort parse of the "available" flag; default to true on success. - let s = String::from_utf8_lossy(&o.stdout); - !s.contains("\"available\":false") - } - Ok(o) => { - let s = String::from_utf8_lossy(&o.stdout); - if s.contains("\"available\":false") { false } else { true } - } - Err(e) => { - warn!(err = %e, "apple-fm-worker: probe spawn failed"); - false - } - } -} diff --git a/hives/apple-fm-worker/src/main.rs b/hives/apple-fm-worker/src/main.rs deleted file mode 100644 index 6f67ae90..00000000 --- a/hives/apple-fm-worker/src/main.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! apple-fm-worker — standalone worker-bee process. -//! -//! Registers with humd as `bee:["worker"]`, advertises the on-device model id, -//! and routes prompts to Apple's on-device 3B FoundationModel via a small -//! Swift bridge (`bridge/gen`) that wraps `FoundationModels`. No network, -//! no API key. -//! -//! Environment: -//! APPLE_FM_MODEL — advertised model id (default: apple-3b-on-device) -//! APPLE_FM_BRIDGE — path to the bridge binary (default: /bridge/gen, -//! resolved relative to this binary; override to test) - -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Result; -use hum_nest::{serve_worker, HiveAdvert}; - -use lib::AppleFmWorker; -mod lib; - -#[tokio::main] -async fn main() -> Result<()> { - hum_paths::init(); - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), - ) - .init(); - - let model_id = - std::env::var("APPLE_FM_MODEL").unwrap_or_else(|_| "apple-3b-on-device".into()); - - // Resolve the bridge next to this binary: /../bridge/gen. - let bridge = resolve_bridge(); - tracing::info!(bridge = %bridge.display(), "apple-fm-worker: bridge resolved"); - - let worker = Arc::new(AppleFmWorker { - model_id: model_id.clone(), - bridge, - }); - - let advert = HiveAdvert { - hive: "apple-fm-worker".to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - models: vec![model_id], - source: Some( - "https://github.com/adiled/hum/tree/main/hives/apple-fm-worker".to_string(), - ), - }; - - serve_worker(worker, advert).await -} - -fn resolve_bridge() -> PathBuf { - if let Ok(p) = std::env::var("APPLE_FM_BRIDGE") { - return PathBuf::from(p); - } - if let Ok(exe) = std::env::current_exe() { - // <...>/apple-fm-worker → <...>/../bridge/gen - let sibling = exe - .parent() - .map(|p| p.join("bridge/gen")) - .unwrap_or_else(|| PathBuf::from("bridge/gen")); - if sibling.exists() { - return sibling; - } - } - // Fallback: relative to CWD (common in dev). - PathBuf::from("bridge/gen") -} - -/// No-op kept for clarity: the bridge is only ever invoked on macOS, and the -/// capability probe inside `raise` handles the "Apple Intelligence off" case. -#[cfg(not(target_os = "macos"))] -fn resolve_bridge() -> std::path::PathBuf { - // Not on macOS — the bridge doesn't exist. Return a sentinel path; - // the probe inside `raise` will fail gracefully and humd will stop routing. - std::path::PathBuf::from("/dev/null") -} diff --git a/hives/apple-fm/.gitignore b/hives/apple-fm/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/hives/apple-fm/.gitignore @@ -0,0 +1 @@ +/target diff --git a/hives/apple-fm-worker/Cargo.lock b/hives/apple-fm/Cargo.lock similarity index 51% rename from hives/apple-fm-worker/Cargo.lock rename to hives/apple-fm/Cargo.lock index b490f18e..ce5a9b6f 100644 --- a/hives/apple-fm-worker/Cargo.lock +++ b/hives/apple-fm/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -31,6 +66,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -38,22 +82,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] -name = "apple-fm-worker" -version = "0.1.0" +name = "apple-fm" +version = "0.34.1" dependencies = [ "anyhow", "async-trait", "hum-identity", "hum-nest", "hum-paths", + "hum-thrum", + "humd", "serde", "serde_json", + "tempfile", + "thrum-core", "tokio", "tokio-util", "tracing", "tracing-subscriber", ] +[[package]] +name = "arc-swap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + [[package]] name = "async-trait" version = "0.1.92" @@ -65,12 +128,35 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http", + "log", + "url", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -79,9 +165,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.18.2" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" +checksum = "048006790404103c488e6c5616b3e7e702a71292aadda08e8f7698a9e0d2eb53" dependencies = [ "aws-lc-sys", "zeroize", @@ -89,9 +175,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.46.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" +checksum = "8bfbe32ac36b3c428696ce315464181bc4029d689fd58e3f2a4e58c252242f52" dependencies = [ "cc", "cmake", @@ -155,6 +241,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64" version = "0.23.1" @@ -188,6 +297,19 @@ version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -197,6 +319,24 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "borrow-or-share" version = "0.2.4" @@ -223,9 +363,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" dependencies = [ "find-msvc-tools", "jobserver", @@ -233,12 +373,57 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + [[package]] name = "cmake" version = "0.1.58" @@ -248,10 +433,24 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.21", +] + [[package]] name = "codegen" -version = "0.32.0" -source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" +version = "0.34.1" dependencies = [ "anyhow", "regex", @@ -286,6 +485,37 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cordyceps" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9ab7e0ca1d179628fa0172b2b97203c7fa0cd81be2448bd446fb9559ca9261" +dependencies = [ + "loom", + "tracing", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -311,6 +541,30 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + [[package]] name = "crypto-common" version = "0.1.7" @@ -321,6 +575,34 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2376ffd6f96d8a9db59875b4b6b0237d6900b5baf64279714e05493ce64828a" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -328,11 +610,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", "curve25519-dalek-derive", - "digest", - "fiat-crypto", + "digest 0.11.3", + "fiat-crypto 0.3.0", + "rand_core 0.10.1", "rustc_version", + "serde", "subtle", "zeroize", ] @@ -348,86 +648,272 @@ dependencies = [ "syn 2.0.119", ] +[[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" +[[package]] +name = "data-encoding-macro" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a127ecbb3c4632e1525380e04c0c3fcf8dcb44d32a79ea290d8a36906edcd8" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" +dependencies = [ + "data-encoding", + "syn 3.0.9", +] + [[package]] name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "zeroize", ] [[package]] -name = "digest" -version = "0.10.7" +name = "der" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "6a996a108feb8b427a6e12fcf6af4aa83409b79d72c15337d7741368131ee004" dependencies = [ - "block-buffer", - "crypto-common", + "const-oid 0.10.2", + "pem-rfc7468", + "zeroize", ] [[package]] -name = "displaydoc" -version = "0.2.7" +name = "deranged" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case", "proc-macro2", "quote", - "syn 3.0.9", + "rustc_version", + "syn 2.0.119", + "unicode-xid", ] [[package]] -name = "dunce" -version = "1.0.5" +name = "diatomic-waker" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" [[package]] -name = "ed25519" -version = "2.2.3" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "pkcs8", - "signature", + "block-buffer 0.10.4", + "crypto-common 0.1.7", ] [[package]] -name = "ed25519-dalek" -version = "2.2.0" +name = "digest" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "curve25519-dalek", - "ed25519", - "rand_core", - "serde", - "sha2", - "subtle", - "zeroize", + "block-buffer 0.12.1", + "crypto-common 0.2.2", ] [[package]] -name = "email_address" -version = "0.2.9" +name = "dispatch2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "serde", + "bitflags", + "block2", + "libc", + "objc2", ] [[package]] -name = "equivalent" -version = "1.0.3" +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8 0.11.0", + "serdect", + "signature 3.0.0", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek 5.0.0", + "ed25519 3.0.0", + "rand_core 0.10.1", + "serde", + "sha2 0.11.1", + "signature 3.0.0", + "subtle", + "zeroize", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "ensemble" +version = "0.34.1" +dependencies = [ + "anyhow", + "async-trait", + "ed25519-dalek 2.2.0", + "hex", + "hum-identity", + "iroh", + "lru 0.12.5", + "parking_lot", + "rand 0.8.8", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 1.0.69", + "thrum-core", + "tokio", + "tokio-rustls", + "tracing", +] + +[[package]] +name = "enum-assoc" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0590c4a94da3372e83493b956755a6e2266830b6e4e3b101afe66e3f39477b91" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "equivalent" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" @@ -452,17 +938,29 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "find-msvc-tools" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" [[package]] name = "fluent-uri" @@ -518,6 +1016,34 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-buffered" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin", +] + [[package]] name = "futures-channel" version = "0.3.34" @@ -534,12 +1060,47 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "futures-sink" version = "0.3.34" @@ -552,14 +1113,22 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -567,6 +1136,21 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be81b31c9ce4caf7f3178eeb90662bb1864d3ad363ec6bdada3201adff19c37" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result 0.4.1", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -584,8 +1168,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -609,8 +1195,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "governor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.8.8", + "smallvec", + "spinning_top", ] [[package]] @@ -632,6 +1263,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -659,6 +1299,9 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -666,6 +1309,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + [[package]] name = "hex" version = "0.4.3" @@ -722,52 +1371,36 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hum-identity" -version = "0.32.0" -source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" +version = "0.34.1" dependencies = [ "anyhow", - "ed25519-dalek", + "ed25519-dalek 2.2.0", "hex", "hum-paths", - "rand", - "serde", - "sha2", - "tracing", - "uuid", -] - -[[package]] -name = "hum-mcp" -version = "0.32.0" -source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" -dependencies = [ - "anyhow", - "axum", - "parking_lot", + "rand 0.8.8", "serde", "serde_json", - "thiserror 1.0.69", - "thrum-core", - "tokio", + "sha2 0.10.9", "tracing", + "uuid", ] [[package]] name = "hum-nest" -version = "0.32.0" -source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" +version = "0.34.1" dependencies = [ "anyhow", "async-trait", + "axum", "command-group", "hum-identity", - "hum-mcp", "hum-paths", "hum-thrum", "libc", - "lru", + "lru 0.12.5", "metrics", - "rand", + "parking_lot", + "rand 0.8.8", "regex", "serde", "serde_json", @@ -780,8 +1413,7 @@ dependencies = [ [[package]] name = "hum-paths" -version = "0.32.0" -source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" +version = "0.34.1" dependencies = [ "jsonschema", "serde", @@ -791,17 +1423,53 @@ dependencies = [ [[package]] name = "hum-thrum" -version = "0.32.0" -source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" +version = "0.34.1" +dependencies = [ + "anyhow", + "hum-identity", + "hum-paths", + "rand 0.8.8", + "serde", + "serde_json", + "thrum-core", + "tokio", + "tracing", +] + +[[package]] +name = "humd" +version = "0.34.1" dependencies = [ "anyhow", + "async-trait", + "chrono", + "ed25519-dalek 2.2.0", + "ensemble", + "governor", + "hex", "hum-identity", + "hum-nest", "hum-paths", - "rand", + "metrics", + "metrics-exporter-prometheus", + "parking_lot", + "rand 0.8.8", + "serde", "serde_json", + "thehum", "thrum-core", "tokio", "tracing", + "tracing-subscriber", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", ] [[package]] @@ -847,7 +1515,7 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" dependencies = [ - "base64", + "base64 0.23.1", "bytes", "futures-channel", "futures-util", @@ -866,12 +1534,36 @@ dependencies = [ ] [[package]] -name = "icu_collections" -version = "2.3.0" +name = "iana-time-zone" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ - "displaydoc", + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", "potential_utf", "utf8_iter", "yoke", @@ -881,9 +1573,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" dependencies = [ "displaydoc", "litemap", @@ -894,9 +1586,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -908,15 +1600,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" [[package]] name = "icu_properties" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" dependencies = [ "displaydoc", "icu_collections", @@ -929,15 +1621,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" [[package]] name = "icu_provider" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" dependencies = [ "displaydoc", "icu_locale_core", @@ -948,6 +1640,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "identity-hash" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" + [[package]] name = "idna" version = "1.1.0" @@ -969,6 +1667,26 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "igd-next" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7238d487a9aff61f81b5ab41c0a841532a115a398b5fa92a2fadd0885e2581" +dependencies = [ + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.10.3", + "tokio", + "url", + "xmltree", +] + [[package]] name = "indexmap" version = "2.14.2" @@ -979,18 +1697,220 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result 0.4.1", + "windows-sys 0.61.2", +] + [[package]] name = "ipnet" version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" +[[package]] +name = "iroh" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3689cab508af9bcef7816c1201b403595ec6aa20e7d15e1f9683bb4be4a5b17" +dependencies = [ + "backon", + "blake3", + "bytes", + "cfg_aliases", + "ctutils", + "data-encoding", + "derive_more", + "ed25519-dalek 3.0.0", + "futures-util", + "getrandom 0.4.3", + "http", + "ipnet", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "iroh-relay", + "n0-error", + "n0-future", + "n0-watcher", + "netwatch", + "noq", + "noq-proto", + "noq-udp", + "papaya", + "pin-project", + "portable-atomic", + "portmapper", + "rand 0.10.3", + "reqwest", + "rustc-hash", + "rustls", + "rustls-pki-types", + "serde", + "smallvec", + "strum 0.28.0", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", +] + +[[package]] +name = "iroh-base" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b72366b16001af3a51574233df88ca6d62da0215807549a769e7de887da803" +dependencies = [ + "curve25519-dalek 5.0.0", + "data-encoding", + "data-encoding-macro", + "derive_more", + "ed25519-dalek 3.0.0", + "getrandom 0.4.3", + "n0-error", + "rand 0.10.3", + "serde", + "url", + "zeroize", +] + +[[package]] +name = "iroh-dns" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7481d0792dde1035a2b06ec06ed3cf6d01fcb1a494e6931b489e32073382048d" +dependencies = [ + "arc-swap", + "cfg_aliases", + "derive_more", + "iroh-base", + "n0-dns-resolver", + "n0-error", + "n0-future", + "portable-atomic", + "rand 0.10.3", + "rustls", + "simple-dns", + "strum 0.28.0", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "iroh-metrics" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ede55536349842337f7cdc63012ec33dc637945f3259c98a0763aac364cdf09" +dependencies = [ + "iroh-metrics-derive", + "itoa", + "n0-error", + "portable-atomic", + "ryu", + "serde", + "tracing", +] + +[[package]] +name = "iroh-metrics-derive" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae5f0c4405d1fbc9fb16ff422ca40620e93dc36c30ecaba0c2aee3992b7bd48" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "iroh-relay" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5589e7c1475700686603a970275687f760df853bc6525c7eb1347aa31d7230" +dependencies = [ + "blake3", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more", + "getrandom 0.4.3", + "http", + "http-body-util", + "hyper", + "hyper-util", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "lru 0.18.5", + "n0-error", + "n0-future", + "noq", + "noq-proto", + "num_enum", + "percent-encoding", + "pin-project", + "postcard", + "rand 0.10.3", + "reqwest", + "rustls", + "rustls-pki-types", + "serde", + "serde_bytes", + "strum 0.28.0", + "tokio", + "tokio-rustls", + "tokio-util", + "tokio-websockets", + "tracing", + "url", + "webpki-roots", + "ws_stream_wasm", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni" version = "0.22.4" @@ -1000,7 +1920,7 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys", + "jni-sys 0.4.1", "log", "simd_cesu8", "thiserror 2.0.21", @@ -1021,6 +1941,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + [[package]] name = "jni-sys" version = "0.4.1" @@ -1052,9 +1981,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.106" +version = "0.3.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" dependencies = [ "cfg-if", "futures-util", @@ -1111,6 +2040,12 @@ version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.3" @@ -1128,9 +2063,22 @@ dependencies = [ [[package]] name = "log" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] [[package]] name = "lru" @@ -1141,6 +2089,27 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "mac-addr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" + [[package]] name = "matchers" version = "0.2.0" @@ -1172,6 +2141,41 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "metrics-exporter-prometheus" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f0c8427b39666bf970460908b213ec09b3b350f20c0c2eabcbba51704a08e6" +dependencies = [ + "base64 0.22.1", + "http-body-util", + "hyper", + "hyper-util", + "indexmap", + "ipnet", + "metrics", + "metrics-util", + "quanta", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-util" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4259040465c955f9f2f1a4a8a16dc46726169bca0f88e8fb2dbeced487c3e828" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.14.5", + "metrics", + "num_cpus", + "quanta", + "sketches-ddsketch", +] + [[package]] name = "micromap" version = "0.3.0" @@ -1196,74 +2200,407 @@ dependencies = [ ] [[package]] -name = "nix" -version = "0.27.1" +name = "n0-dns-resolver" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "535c99c9a786bee714155d2ee4fc2477405c2c43536e1bc6a51de09a0d1448f7" dependencies = [ - "bitflags", - "cfg-if", - "libc", + "derive_more", + "ipconfig", + "jni 0.22.4", + "lru 0.18.5", + "n0-error", + "n0-future", + "ndk-context", + "rand 0.10.3", + "reqwest", + "rustc-hash", + "rustls", + "simple-dns", + "system-configuration", + "tokio", + "tokio-rustls", + "tracing", + "webpki-roots", ] [[package]] -name = "ntapi" -version = "0.4.3" +name = "n0-error" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +checksum = "26c59ea174675ce6bc196878851e5049e11b8b1f9af3ba87e4d6df8d828bb001" dependencies = [ - "winapi", + "n0-error-macros", + "spez", ] [[package]] -name = "nu-ansi-term" -version = "0.50.3" +name = "n0-error-macros" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +checksum = "4412346410d6616f3668c40e53c298e5320c7b856f7a960e5914e442601be79f" dependencies = [ - "windows-sys 0.61.2", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "num" -version = "0.4.3" +name = "n0-future" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", + "cfg_aliases", + "derive_more", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", ] [[package]] -name = "num-bigint" -version = "0.4.8" +name = "n0-watcher" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +checksum = "64bf1ecb076a87e0e3f5edcf726e7d29e788c273bb0161ba30666b53f085906c" dependencies = [ - "num-integer", - "num-traits", + "derive_more", + "n0-error", + "n0-future", + "slotmap", ] [[package]] -name = "num-cmp" -version = "0.1.0" +name = "ndk-context" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] -name = "num-complex" -version = "0.4.6" +name = "netdev" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +checksum = "096c44b66a9b09b99b4dd36b1c295ff3a492039ccecaf55466bd6ddcdba59968" dependencies = [ - "num-traits", -] - -[[package]] + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "jni 0.21.1", + "libc", + "mac-addr", + "ndk-context", + "netlink-packet-core 0.8.2", + "netlink-packet-route 0.31.0", + "netlink-sys 0.8.8", + "objc2", + "objc2-core-foundation", + "objc2-core-wlan", + "objc2-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.61.2", +] + +[[package]] +name = "netdev" +version = "0.46.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c52c2584961c68f5a6e3356797344061f818c93b8acc9cd6d7e284795e563e" +dependencies = [ + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "jni 0.21.1", + "libc", + "mac-addr", + "ndk-context", + "netlink-packet-core 0.9.0", + "netlink-packet-route 0.33.0", + "netlink-sys 0.9.0", + "objc2-core-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.61.2", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b897d7bd4f0af82e68d40d0344cf37e97f9c97ddf74a098de3e4da05e96ca395" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6e2daf9a8c2e21302714706860a0e6be02e03d17294ecc66b354ea7d4059dd" + +[[package]] +name = "netlink-packet-route" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2288fcb784eb3defd5fb16f4c4160d5f477de192eac730f43e1d11c24d9a007" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core 0.8.2", +] + +[[package]] +name = "netlink-packet-route" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59d48ffc8b73a506e1ff0878528c72668f2bfbc3d875b6be890a96be5d0d5576" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core 0.9.0", + "zerocopy", +] + +[[package]] +name = "netlink-proto" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93af8261786086024cd5e96e0a991dd65ced07bbf7c233a487bbc96b971d5539" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "log", + "netlink-packet-core 0.8.2", + "netlink-sys 0.8.8", + "thiserror 2.0.21", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "netlink-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99d38e00b420df49e940fe0826e667c3dad82ac68eb4c2bbf754c1c14a83a10" +dependencies = [ + "bytes", + "libc", + "log", +] + +[[package]] +name = "netwatch" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39da9cad5f23aa43401f09497d3b280e51b5feba115d9ffbf38ca35d6ff95e96" +dependencies = [ + "atomic-waker", + "bytes", + "cfg_aliases", + "derive_more", + "ipnet", + "js-sys", + "libc", + "n0-error", + "n0-future", + "n0-watcher", + "netdev 0.45.1", + "netdev 0.46.3", + "netlink-packet-core 0.8.2", + "netlink-packet-route 0.31.0", + "netlink-proto", + "netlink-sys 0.8.8", + "noq-udp", + "objc2-core-foundation", + "objc2-system-configuration", + "pin-project-lite", + "serde", + "socket2", + "time", + "tokio", + "tokio-util", + "tracing", + "web-sys", + "windows 0.62.2", + "windows-result 0.4.1", + "wmi", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + +[[package]] +name = "noq" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6249a065adc1edbc1704a95436f38dd60476064dbc3b7c47cf279e02d1590eb5" +dependencies = [ + "bytes", + "cfg_aliases", + "derive_more", + "noq-proto", + "noq-udp", + "pin-project-lite", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.21", + "tokio", + "tokio-stream", + "tracing", + "web-time", +] + +[[package]] +name = "noq-proto" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01a35be3ed37558c8db69554419f24eff8a63599b54f711bbe3863ec727c47d" +dependencies = [ + "aes-gcm", + "bytes", + "derive_more", + "enum-assoc", + "getrandom 0.4.3", + "identity-hash", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "sorted-index-buffer", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "noq-udp" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0160422832eea1cc11c34be04ee6d5a046452b0b46cfb14695d55a1f24c954" +dependencies = [ + "cfg_aliases", + "libc", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] name = "num-integer" version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1302,17 +2639,145 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-core-wlan" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-security", + "objc2-security-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef76382e9cedd18123099f17638715cc3d81dba3637d4c0d39ab69df2ef345a5" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +checksum = "7e036b411754132fd6b74e94b74e9a13c5cf606fa93d1885c35860d3060820b1" [[package]] name = "outref" @@ -1320,6 +2785,22 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "papaya" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2442474a9404698c42509b8967f437249dbc7b50493e83020333d3943ec0ae" +dependencies = [ + "equivalent", + "seize", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1343,12 +2824,57 @@ dependencies = [ "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1359,23 +2885,114 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.3", + "spki 0.8.1", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327a4cb2ad49dc48adfdc9da89e22c64d488b508c67a044d71ba729e4521a285" +dependencies = [ + "base64 0.23.1", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" +dependencies = [ + "serde", +] + +[[package]] +name = "portmapper" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca97242f016e090a25330613bc2382aab65eb22f9149dbe84d8f8e711d49a530" dependencies = [ - "der", - "spki", + "base64 0.22.1", + "bytes", + "derive_more", + "hyper-util", + "igd-next", + "iroh-metrics", + "libc", + "n0-error", + "n0-future", + "netwatch", + "num_enum", + "rand 0.10.3", + "serde", + "smallvec", + "socket2", + "time", + "tokio", + "tokio-util", + "tower-layer", + "tracing", + "url", ] [[package]] -name = "pkg-config" -version = "0.3.34" +name = "postcard" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "postcard-derive", + "serde", +] [[package]] -name = "portable-atomic" -version = "1.15.0" +name = "postcard-derive" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "potential_utf" @@ -1386,6 +3003,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1395,6 +3018,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -1404,6 +3036,30 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.47" @@ -1433,7 +3089,18 @@ checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -1443,7 +3110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1455,6 +3122,30 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1536,7 +3227,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64", + "base64 0.23.1", "bytes", "futures-channel", "futures-core", @@ -1560,12 +3251,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", ] @@ -1583,6 +3276,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1592,6 +3291,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.46" @@ -1599,7 +3311,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "aws-lc-rs", + "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -1624,6 +3338,7 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ + "web-time", "zeroize", ] @@ -1635,7 +3350,7 @@ checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" dependencies = [ "core-foundation", "core-foundation-sys", - "jni", + "jni 0.22.4", "log", "once_cell", "rustls", @@ -1656,9 +3371,9 @@ checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "aws-lc-rs", "ring", @@ -1696,6 +3411,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -1725,12 +3446,24 @@ dependencies = [ "libc", ] +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" + [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.229" @@ -1741,6 +3474,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -1797,6 +3540,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1_smol" version = "1.0.1" @@ -1810,8 +3563,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] @@ -1845,7 +3609,16 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "rand_core 0.10.1", ] [[package]] @@ -1864,17 +3637,41 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simple-dns" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6f884fa9a8d48101774bfbd3aeb81e968dd22cffd19a372da69f183db22c1a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "sketches-ddsketch" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" -version = "1.16.3" +version = "1.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" [[package]] name = "socket2" @@ -1886,6 +3683,38 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "sorted-index-buffer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06" + +[[package]] +name = "spez" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + +[[package]] +name = "spinning_top" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf02766a1eb909bc7653557c66b384e44edc3ad979e6257c6173c65ea51bc7d" +dependencies = [ + "lock_api", +] + [[package]] name = "spki" version = "0.7.3" @@ -1893,7 +3722,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" +dependencies = [ + "base64ct", + "der 0.8.3", ] [[package]] @@ -1908,7 +3747,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -1924,6 +3772,18 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "subtle" version = "2.6.1" @@ -1982,7 +3842,63 @@ dependencies = [ "libc", "memchr", "ntapi", - "windows", + "windows 0.57.0", +] + +[[package]] +name = "system-configuration" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501336eb7ba9e417300a6a0fa985721065467aa83a6dcf0422a8e43e4c0328fa" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thehum" +version = "0.34.1" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "ed25519-dalek 2.2.0", + "ensemble", + "hex", + "hum-identity", + "hum-paths", + "parking_lot", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 1.0.69", + "thrum-core", + "tokio", + "tracing", ] [[package]] @@ -2036,8 +3952,7 @@ dependencies = [ [[package]] name = "thrum-core" -version = "0.32.0" -source = "git+https://github.com/adiled/hum.git?branch=main#febc86220edf78b95fb842b43c2a454a73e9b9b1" +version = "0.34.1" dependencies = [ "codegen", "hex", @@ -2045,26 +3960,63 @@ dependencies = [ "parking_lot", "serde", "serde_json", - "sha2", - "strum", + "sha2 0.10.9", + "strum 0.26.3", "thiserror 1.0.69", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "js-sys", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" dependencies = [ "displaydoc", "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + [[package]] name = "tokio" -version = "1.53.3" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", @@ -2094,8 +4046,20 @@ version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" dependencies = [ - "rustls", + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -2107,11 +4071,65 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "libc", "pin-project-lite", "tokio", ] +[[package]] +name = "tokio-websockets" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52efb639344a7c6adb8e62c6f3d2c19c001ff1b79a5041ba1c6ed42e19c6aa5" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-sink", + "getrandom 0.4.3", + "http", + "httparse", + "rand 0.10.3", + "ring", + "rustls-pki-types", + "sha1_smol", + "simdutf8", + "tokio", + "tokio-rustls", + "tokio-util", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + [[package]] name = "tower" version = "0.5.3" @@ -2244,6 +4262,28 @@ version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -2260,19 +4300,29 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] name = "utf8_iter" -version = "1.0.4" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3bd9d3e571c26fcce9d033b87ea5ab061b8d2ffe9b7a5675d810d68f03edf0" +dependencies = [ + "utf_types", +] + +[[package]] +name = "utf_types" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +checksum = "3a3b425286980a367278453f3b6d28cdcb134b574e5a35824a6ff5eebad50bb6" [[package]] name = "uuid" -version = "1.28.0" +version = "1.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" dependencies = [ "js-sys", "sha1_smol", @@ -2343,12 +4393,11 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.129" +version = "0.2.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" dependencies = [ "cfg-if", - "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", @@ -2356,9 +4405,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.79" +version = "0.4.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +checksum = "5e14e0327cb5368ad60ccb22d04a9827a693c95a926d8d125e9fa39e0baec501" dependencies = [ "js-sys", "tokio", @@ -2367,9 +4416,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.129" +version = "0.2.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2377,9 +4426,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.129" +version = "0.2.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" dependencies = [ "bumpalo", "proc-macro2", @@ -2390,18 +4439,41 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.129" +version = "0.2.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" -version = "0.3.106" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d179596ff5ec0f6de4db64d54011bde6289cc49603d3a6e4ce3f329207ab88" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -2409,13 +4481,28 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a" dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -2453,8 +4540,29 @@ version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ - "windows-core", - "windows-targets", + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", ] [[package]] @@ -2463,10 +4571,34 @@ version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-targets", + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", ] [[package]] @@ -2480,6 +4612,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-interface" version = "0.57.0" @@ -2491,19 +4634,78 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", ] [[package]] @@ -2512,7 +4714,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2524,34 +4726,76 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2564,47 +4808,129 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wmi" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.21", + "windows 0.62.2", + "windows-core 0.62.2", +] + [[package]] name = "writeable" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper", + "thiserror 2.0.21", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "yoke" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -2669,6 +4995,20 @@ name = "zeroize" version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "zerotrie" @@ -2683,9 +5023,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" dependencies = [ "yoke", "zerofrom", @@ -2694,9 +5034,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" dependencies = [ "proc-macro2", "quote", diff --git a/hives/apple-fm/Cargo.toml b/hives/apple-fm/Cargo.toml new file mode 100644 index 00000000..7d3382c8 --- /dev/null +++ b/hives/apple-fm/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "apple-fm" +version = "0.34.1" +edition = "2024" +license = "MIT" +description = "Apple's on-device foundation model as a hum nurse: local, network-free and key-free, through a small Swift bridge to FoundationModels." + +[dependencies] +thrum-core = { path = "../../thrum-core" } +hum-paths = { path = "../../hum-paths" } +hum-nest = { path = "../../hum-nest" } +tokio = { version = "1", features = ["full"] } +tokio-util = "0.7" +async-trait = "0.1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +anyhow = "1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[dev-dependencies] +humd = { path = "../../humd" } +hum-identity = { path = "../../hum-identity" } +hum-thrum = { path = "../../hum-thrum" } +tempfile = "3" + +[[bin]] +name = "apple-fm-nurse" +path = "src/main.rs" +[workspace] diff --git a/hives/apple-fm/Orchfile b/hives/apple-fm/Orchfile new file mode 100644 index 00000000..0be6b45d --- /dev/null +++ b/hives/apple-fm/Orchfile @@ -0,0 +1,27 @@ +# apple-fm: Apple's on-device foundation model as a nurse, grown through +# apple-fm-gen, the Swift bridge ./build makes beside it. Set an ARG in +# the environment of `hum hive apple-fm install` to change it, for +# example APPLE_FM_MODEL=apple-on-device. An empty ARG keeps the bee's +# own default; the README says what each one is. +# +# The nurse waits unnestled while Apple Intelligence is off or its model +# is still downloading, and on a Mac that cannot run the model it exits +# 0, so its unit stays down; RESTART on-failure restarts it only when it +# fails. + +ARG HUM_SRC= +ARG APPLE_FM_MODEL= +ARG APPLE_FM_PROBE_MS= + +SERVICE hive-install +RUN ${HUM_SRC}/install +ONESHOT true + +SERVICE apple-fm +RUN ${HUM_SRC}/target/release/apple-fm-nurse +ENV APPLE_FM_MODEL=${APPLE_FM_MODEL} +ENV APPLE_FM_PROBE_MS=${APPLE_FM_PROBE_MS} +RESTART on-failure +RESTART_DELAY 5s +START_LIMIT_BURST 3 +START_LIMIT_INTERVAL 1m diff --git a/hives/apple-fm/README.md b/hives/apple-fm/README.md new file mode 100644 index 00000000..b3e3cee0 --- /dev/null +++ b/hives/apple-fm/README.md @@ -0,0 +1,207 @@ +--- +title: "apple-fm (Rust + Swift)" +description: "Apple's on-device foundation model as a nurse: local, network-free and key-free, through a small Swift bridge to FoundationModels" +--- + +# apple-fm (Rust + Swift) + +> _Apple's on-device foundation model as a nurse: local, network-free and key-free, through a small Swift bridge to FoundationModels_ + +The hive has one bee, `apple-fm-nurse`, entered as a nurse under the +label `main`. It claims one model id, `apple-3b-on-device`, and grows +each bloom through the language model Apple ships with macOS 26 +(`SystemLanguageModel.default`, about 3B parameters, a 4096-token +context), on this Mac itself. Nothing leaves the +machine, and there is no key. + +The nurse is Rust, on [`hum-nest`](../../hum-nest). It reaches Apple +through `apple-fm-gen`, a Swift binary built from +[`bridge/gen.swift`](bridge/gen.swift) and run once per bloom. That one +file is the only code in hum that imports an Apple framework: the Rust +side holds no Apple symbol and builds anywhere, and the hive's `build` +refuses any host but an Apple silicon Mac. The hive is its own cargo +workspace, outside the repo root's. + +## Propensity + +| bee | statefulness | richness | wire | +|---|---|---|---| +| `apple-fm-nurse` | `stateful_session` | none claimed | `apple-fm` | + +It speaks what every hum-nest nurse does: `hello`, `prompt`, `cancel`, +`cleanup`, `curate`, `tendril-result`, `unfurl`, `wilt`, `error`, +`tendril-reach`, `tendril-info` and `pulse`. + +## The nurse + +| env | default | what | +|---|---|---| +| `APPLE_FM_MODEL` | `apple-3b-on-device` | the model id it claims | +| `APPLE_FM_LABEL` | `main` | the label its bee is entered as; a second nurse beside it needs its own | +| `APPLE_FM_BRIDGE` | `apple-fm-gen` beside the nurse's binary | the bridge it runs | +| `APPLE_FM_PROBE_MS` | `60000` | how often a waiting nurse asks again whether the model is ready | + +An empty `APPLE_FM_*` variable is no variable too. + +**Before it nestles** the nurse asks the bridge whether Apple's model +can run here (`apple-fm-gen --probe`). When it can, the nurse nestles. +While Apple Intelligence is off (System Settings → Apple Intelligence & +Siri) or its model is still downloading, it waits without nestling, +logs `apple-fm.dormant` with the reason once, and asks again every +`APPLE_FM_PROBE_MS`, nestling as soon as the model is ready. On a Mac +that cannot run the model at all it logs `apple-fm.never` and exits 0, +so its unit stays down. A bridge that cannot be run exits it non-zero. + +A bloom grows through one run of the bridge: + +- **The conversation.** The system prompt becomes Apple's instructions. + The blooms before the prompt become Apple's transcript, each bloom's + words as a prompt and the text it grew as the response, and the + prompt's own words are what the model answers. A prompt that carries + a graft is grown from its graft. One that carries none, as from a + forager that sends only a sigil's new words, is grown from the turns + its cell has grown so far, so a sigil keeps its conversation either + way. +- **How it grows.** `temperature` and `season` become Apple's + `temperature` and `maximumResponseTokens`. A `temperature` of 0 + samples greedily; `prune.k` samples from the top k, else `prune.p` + from the top p, each with the prompt's `seed`; a `seed` alone samples + from them all. `frost` is kept by the nurse: the bloom unfurls up to + the first stop it meets and wilts there, and the bridge is killed. + `sepal` is ignored, since the model does not think aloud. +- **The reply.** Its words unfurl as a text petal as the model grows + them, each unfurl only the words new since the last. The bloom wilts + `stop`, or `max_tokens` when it grew all of its `season`, with Apple's + token counts as nectar (macOS 26.4 and later). A `cancel` kills the + bridge and wilts it `cancel`. +- **Refused.** Apple's model grows words from words, so a prompt whose + petals carry pollen is refused with `apple_fm_words_only`, and one + with a `trellis` with `apple_fm_no_trellis`. The nurse does not grow + on: a prompt with no words is refused by its nest with + `cannot-grow-on`, or `empty-prompt` when it carries no graft either. +- **Errors.** Apple's own become `apple_fm_`: + `apple_fm_exceeded_context_window` past the model's 4096 tokens, + `apple_fm_guardrail_violation`, `apple_fm_refusal`, + `apple_fm_unsupported_language`, `apple_fm_rate_limited`, + `apple_fm_assets_unavailable`, and `apple_fm_unavailable` when the + model stopped being ready after the nurse nestled. The nurse adds + `apple_fm_bridge_spawn` when the bridge cannot be run, + `apple_fm_bridge_died` when it ends without finishing (with its exit + status and the end of its stderr), and `apple_fm_cell_stilled` for a + prompt its cell was stilled before it grew. + +The nurse offers the model no tools, so its blooms reach no tendrils. + +Its nest's limits are in the `nest` section of +`$XDG_CONFIG_HOME/hum/hives/apple-fm.json`. A cell lives for its sigil, +remembering the turns it grew and growing each prompt the nest hands it +there in turn, until the nest ends it: past `maxCells` (default 8, and +the headroom the nurse claims) as the least recently used, after +`idleMs` (default five minutes) idle, or past `bounds.wallClockMs`. +However a cell ends, the bloom growing in it wilts `cancel` and its +bridge is killed. The other `bounds` cap nothing here. + +### The bridge + +`apple-fm-gen` reads one JSON line on stdin and writes NDJSON on +stdout, each line as soon as it is known: + +``` +stdin {"instructions": "...", "turns": [{"prompt": "...", "response": "..."}], + "temperature": 0.7, "maximumResponseTokens": 256, + "sampling": {"mode": "greedy" | "top" | "nucleus", "k": 40, "p": 0.9, "seed": 7}, + "prompt": "..."} +stdout {"event": "delta", "text": "..."} + {"event": "finish", "inputTokens": 15, "outputTokens": 75} + {"event": "error", "code": "guardrail_violation", "message": "..."} +``` + +Apple streams each snapshot of the reply whole; the bridge writes only +what is new in it. `apple-fm-gen --probe` reads nothing and writes +`{"event": "probe", "available": true}`, or `false` with a `reason`: +`apple_intelligence_not_enabled`, `model_not_ready` or +`device_not_eligible`. + +```bash +echo '{"prompt": "Say hello in French."}' | target/release/apple-fm-gen +``` + +## Run + +Needs an Apple silicon Mac on macOS 26 or later, with Apple +Intelligence on, and the Command Line Tools' `swiftc` (no Xcode). + +```bash +hum hive apple-fm install +``` + +`install` builds the nurse (`cargo build --release --locked`) and the +bridge beside it (`swiftc`, into `target/release/apple-fm-gen`), and +keeps the Orchfile's `apple-fm` unit running under orchd, in the +`hum-apple-fm` namespace. A setting rides in the environment of the +install: + +```bash +APPLE_FM_MODEL=apple-on-device hum hive apple-fm install +``` + +The nurse runs from its directory too, once the hive is installed on +the humd it lands on (it enters its own bee when it starts): + +```bash +cd hives/apple-fm && ./build && target/release/apple-fm-nurse +``` + +## Test + +```bash +cd hives/apple-fm && cargo test +``` + +The nurse's tests run it as orchd would, against a humd in a root of its +own, a forager that prompts through it, and a stand-in bridge: a prompt +unfurls and wilts back with its nectar, the sigil's next prompt grows on +from what its cell grew, a cancel kills the bridge, a nurse waits +unnestled until the model is ready and then grows, one on a Mac that +cannot run the model exits 0, and one whose bridge cannot run fails. +Its cells' tests cover the turns a cell keeps and a graft that overrides +them, a still, a stop, a season that ran out, and each way a bridge can +fail; the unit tests cover the request a prompt becomes, its sampling, +what is refused, what the bridge says, and how `frost` holds back what +may be a stop. + +Against Apple's own model, on a Mac that can run it: + +```bash +./build && cargo test --test live -- --ignored +``` + +It holds a conversation through humd, streams a count with each unfurl +new, stops at a stop, wilts `max_tokens` at its season, and cancels. + +## Use + +Any forager's client, with the model `apple-3b-on-device`. Through +[`ollama-server`](../ollama-server): + +```bash +curl http://localhost:14624/api/chat \ + -H "Content-Type: application/json" \ + -d '{"model": "apple-3b-on-device", "messages": [{"role": "user", "content": "ping"}]}' +``` + +## What is not yet + +- **No tools.** FoundationModels can call tools, but the bridge offers + the model none, so a bloom's tendrils are not reached. +- **No trellis.** FoundationModels can hold a reply to a schema, but the + bridge grows free text only. +- **Not in the mac desktop bundle.** The hive installs on its own; the + desktop app does not yet carry it. + +## See also + +- [`ollama-server`](../ollama-server), [`dwarfstar`](../dwarfstar): sibling nurses for local models. +- [`hum-nest`](../../hum-nest): the nest the nurse runs on. +- [hives](../README.md): how a hive is installed and how its bees enter. +- [Apple's Foundation Models framework](https://developer.apple.com/documentation/foundationmodels). diff --git a/hives/apple-fm/bridge/gen.swift b/hives/apple-fm/bridge/gen.swift new file mode 100644 index 00000000..a21cfc4d --- /dev/null +++ b/hives/apple-fm/bridge/gen.swift @@ -0,0 +1,179 @@ +// apple-fm's bridge, and the only code in hum that speaks to Apple. +// +// One run grows one bloom through Apple's on-device foundation model +// (SystemLanguageModel.default, through FoundationModels). It reads one +// JSON line on stdin: +// +// {"instructions": "...", "turns": [{"prompt": "...", "response": "..."}], +// "prompt": "...", "temperature": 0.7, "maximumResponseTokens": 256, +// "sampling": {"mode": "greedy" | "top" | "nucleus", "k": 40, "p": 0.9, "seed": 7}} +// +// and writes NDJSON on stdout, each line as soon as it is known: +// +// {"event": "delta", "text": "..."} the words new since the last line +// {"event": "finish", "inputTokens": n, "outputTokens": n} +// (counts on macOS 26.4 and later) +// {"event": "error", "code": "...", "message": "..."} +// +// `gen --probe` reads nothing and writes one line, +// {"event": "probe", "available": true|false, "reason": "..."}. +// +// It exits 0 after a finish or a probe, 1 after an error, 2 on a request it +// cannot read. + +import Foundation +import FoundationModels + +struct Turn: Decodable { + let prompt: String + let response: String +} + +struct Sampling: Decodable { + let mode: String + let k: Int? + let p: Double? + let seed: UInt64? +} + +struct Request: Decodable { + let instructions: String? + let turns: [Turn]? + let prompt: String + let temperature: Double? + let maximumResponseTokens: Int? + let sampling: Sampling? +} + +struct Event: Encodable { + let event: String + var text: String? = nil + var available: Bool? = nil + var reason: String? = nil + var code: String? = nil + var message: String? = nil + var inputTokens: Int? = nil + var outputTokens: Int? = nil +} + +@main +struct Gen { + static func say(_ event: Event) { + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes] + guard var line = try? encoder.encode(event) else { return } + line.append(0x0A) + FileHandle.standardOutput.write(line) + } + + static func fail(_ code: String, _ message: String, exit status: Int32 = 1) -> Never { + say(Event(event: "error", code: code, message: message)) + exit(status) + } + + static func unavailable() -> String? { + switch SystemLanguageModel.default.availability { + case .available: + return nil + case .unavailable(.deviceNotEligible): + return "device_not_eligible" + case .unavailable(.appleIntelligenceNotEnabled): + return "apple_intelligence_not_enabled" + case .unavailable(.modelNotReady): + return "model_not_ready" + case .unavailable(let other): + return String(describing: other) + } + } + + static func sampling(_ s: Sampling?) -> GenerationOptions.SamplingMode? { + guard let s else { return nil } + switch s.mode { + case "greedy": return .greedy + case "top": return .random(top: s.k ?? 50, seed: s.seed) + case "nucleus": return .random(probabilityThreshold: s.p ?? 1.0, seed: s.seed) + default: return nil + } + } + + static func code(_ error: LanguageModelSession.GenerationError) -> String { + switch error { + case .exceededContextWindowSize: return "exceeded_context_window" + case .assetsUnavailable: return "assets_unavailable" + case .guardrailViolation: return "guardrail_violation" + case .unsupportedGuide: return "unsupported_guide" + case .unsupportedLanguageOrLocale: return "unsupported_language" + case .decodingFailure: return "decoding_failure" + case .rateLimited: return "rate_limited" + case .concurrentRequests: return "concurrent_requests" + case .refusal: return "refusal" + @unknown default: return "generation_failed" + } + } + + static func text(_ s: String) -> [Transcript.Segment] { + [.text(Transcript.TextSegment(content: s))] + } + + static func main() async { + if CommandLine.arguments.dropFirst().contains("--probe") { + let why = unavailable() + say(Event(event: "probe", available: why == nil, reason: why)) + return + } + guard let line = readLine(strippingNewline: true), + let request = try? JSONDecoder().decode(Request.self, from: Data(line.utf8)) + else { + fail("bad_request", "the bridge reads one JSON request on stdin", exit: 2) + } + if let why = unavailable() { + fail("unavailable", why) + } + + var entries: [Transcript.Entry] = [] + if let instructions = request.instructions, !instructions.isEmpty { + entries.append(.instructions(Transcript.Instructions(segments: text(instructions), toolDefinitions: []))) + } + for turn in request.turns ?? [] { + entries.append(.prompt(Transcript.Prompt(segments: text(turn.prompt)))) + entries.append(.response(Transcript.Response(assetIDs: [], segments: text(turn.response)))) + } + let session = LanguageModelSession(model: .default, transcript: Transcript(entries: entries)) + let options = GenerationOptions( + sampling: sampling(request.sampling), + temperature: request.temperature, + maximumResponseTokens: request.maximumResponseTokens + ) + + var said = "" + var sent = 0 + do { + for try await snapshot in session.streamResponse(to: request.prompt, options: options) { + said = snapshot.content + let scalars = said.unicodeScalars + let count = scalars.count + if count > sent { + var delta = String.UnicodeScalarView() + delta.append(contentsOf: scalars.dropFirst(sent)) + say(Event(event: "delta", text: String(delta))) + sent = count + } + } + } catch let error as LanguageModelSession.GenerationError { + fail(code(error), error.localizedDescription) + } catch { + fail("generation_failed", String(describing: error)) + } + + var finish = Event(event: "finish") + if #available(macOS 26.4, *) { + let asked = entries + [.prompt(Transcript.Prompt(segments: text(request.prompt)))] + finish.inputTokens = try? await SystemLanguageModel.default.tokenCount(for: asked) + if let counted = try? await SystemLanguageModel.default.tokenCount(for: said), + let framing = try? await SystemLanguageModel.default.tokenCount(for: "") { + finish.outputTokens = max(0, counted - framing) + } + } + say(finish) + } +} diff --git a/hives/apple-fm/build b/hives/apple-fm/build new file mode 100755 index 00000000..02014ccc --- /dev/null +++ b/hives/apple-fm/build @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Build this hive's bee: the nurse, and beside it apple-fm-gen, the Swift +# bridge to Apple's on-device model. +# +# The hive is a kind; this makes its material. The hive's own install +# runs this when a humd installs the hive, so the hive needs no knowledge +# outside its own directory, and nothing is installed into the system's +# PATH: the bee is built here and runs here. +# +# FoundationModels ships with macOS 26, so the bridge builds only there, +# with the Command Line Tools' swiftc; no Xcode is needed. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$here" + +if [ "$(uname -s)" != Darwin ] || [ "$(uname -m)" != arm64 ]; then + echo "apple-fm: Apple's on-device model runs only on Apple silicon Macs with macOS 26 or later" >&2 + exit 1 +fi + +cargo build --quiet --release --locked +swiftc -parse-as-library -O -target arm64-apple-macos26.0 bridge/gen.swift -o target/release/apple-fm-gen diff --git a/hives/apple-fm/install b/hives/apple-fm/install new file mode 100755 index 00000000..5805207f --- /dev/null +++ b/hives/apple-fm/install @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Install this hive on a humd: build its bee and keep its unit running +# under orchd. The nurse enters itself under the hive when it starts, and +# waits unnestled while Apple's model is not ready on this Mac. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +hive="${HUM_HIVE:-$(basename "$here")}" +state="${XDG_STATE_HOME:-$HOME/.local/state}/hum/hives/$hive" +orchd="$(command -v orchd || echo "$HOME/.local/bin/orchd")" +mkdir -p "$state" + +"$here/build" + +args=(--arg "HUM_SRC=$here") +while read -r name; do + if [ -n "${!name:-}" ]; then args+=(--arg "$name=${!name}"); fi +done < <(sed -n 's/^ARG \([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p' "$here/Orchfile" | grep -vx -e HUM_SRC -e HUM_HIVE) + +units=() +while read -r unit; do units+=("$unit"); done < <(sed -n 's/^SERVICE \([A-Za-z0-9_.-]*\)$/\1/p' "$here/Orchfile" | grep -vx hive-install) + +run() { "$orchd" --project-dir "$here" --namespace "hum-$hive" --user --state-dir "$state/orch" "${args[@]}" "$@"; } +run fell >/dev/null 2>&1 || true +run grow "${units[@]}" diff --git a/hives/apple-fm/src/bridge.rs b/hives/apple-fm/src/bridge.rs new file mode 100644 index 00000000..df00d505 --- /dev/null +++ b/hives/apple-fm/src/bridge.rs @@ -0,0 +1,350 @@ +//! The bridge's wire: what one run of `apple-fm-gen` is asked, and what it +//! says back. +//! +//! A prompt becomes one JSON line on the bridge's stdin. The prompt's system +//! prompt becomes Apple's instructions, the conversation before it becomes +//! turns, each a prompt and the response it grew, and how the bloom grows +//! becomes Apple's generation options: `temperature`, `season` as the most +//! tokens it may grow, and `prune` and `seed` as how it samples, greedily +//! at a temperature of 0. The bridge answers in NDJSON, each line a +//! [`Said`]. +//! +//! Apple's model grows words from words. A prompt that carries pollen or +//! asks for a `trellis` is refused rather than grown as if it had not. + +use std::path::Path; +use std::time::Duration; + +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; +use thrum_core::{Bloom, Petal, PetalKind, PromptBody}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Turn { + pub prompt: String, + pub response: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(tag = "mode", rename_all = "lowercase")] +pub enum Sampling { + Greedy, + Top { k: u64, seed: Option }, + Nucleus { p: f64, seed: Option }, +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Ask { + #[serde(skip_serializing_if = "Option::is_none")] + pub instructions: Option, + pub turns: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub maximum_response_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sampling: Option, + pub prompt: String, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(tag = "event", rename_all = "lowercase")] +pub enum Said { + Delta { + text: String, + }, + Finish { + #[serde(rename = "inputTokens")] + input_tokens: Option, + #[serde(rename = "outputTokens")] + output_tokens: Option, + }, + Error { + code: String, + message: String, + }, + Probe { + available: bool, + reason: Option, + }, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Refusal { + pub code: &'static str, + pub message: String, +} + +fn is(petal: &Petal, kind: PetalKind) -> bool { + petal.kind == kind.as_str() +} + +pub fn words(petals: &[Petal]) -> String { + petals + .iter() + .filter(|p| is(p, PetalKind::Text)) + .filter_map(|p| p.text.as_deref()) + .collect::>() + .join("\n") +} + +fn grown(petals: &[Petal]) -> String { + petals + .iter() + .filter(|p| is(p, PetalKind::Text)) + .filter_map(|p| p.text.as_deref()) + .collect() +} + +pub fn turns(graft: &[Bloom]) -> Vec { + graft + .iter() + .map(|bloom| Turn { + prompt: words(bloom.content.as_deref().unwrap_or(&[])), + response: grown(bloom.grew.as_deref().unwrap_or(&[])), + }) + .filter(|turn| !turn.prompt.is_empty() || !turn.response.is_empty()) + .collect() +} + +pub fn said_by(prompt: &PromptBody) -> String { + let content = prompt.content.as_deref().unwrap_or(&[]); + let said = words(content); + if !said.is_empty() { + return said; + } + prompt.text.clone().unwrap_or_default() +} + +pub fn sampling(prompt: &PromptBody) -> Option { + if prompt.temperature == Some(0.0) { + return Some(Sampling::Greedy); + } + let seed = prompt.seed.map(|s| s as u64); + let prune = prompt.prune.as_ref(); + if let Some(k) = prune.and_then(|p| p.k) { + return Some(Sampling::Top { k, seed }); + } + if let Some(p) = prune.and_then(|p| p.p) { + return Some(Sampling::Nucleus { p, seed }); + } + seed.map(|seed| Sampling::Nucleus { p: 1.0, seed: Some(seed) }) +} + +pub fn ask(prompt: &PromptBody, before: &[Turn]) -> Result { + let content = prompt.content.as_deref().unwrap_or(&[]); + if content.iter().any(|p| is(p, PetalKind::Pollen)) { + return Err(Refusal { + code: "apple_fm_words_only", + message: "Apple's on-device model grows from words alone, and this prompt carries pollen".into(), + }); + } + if prompt.trellis.is_some() { + return Err(Refusal { + code: "apple_fm_no_trellis", + message: "apple-fm grows free text, and cannot hold a bloom to a trellis".into(), + }); + } + let said = said_by(prompt); + if said.is_empty() { + return Err(Refusal { code: "apple_fm_no_words", message: "apple-fm grows a bloom only from new words".into() }); + } + let sampling = sampling(prompt); + Ok(Ask { + instructions: prompt.system_prompt.clone().filter(|s| !s.is_empty()), + turns: before.to_vec(), + temperature: if sampling == Some(Sampling::Greedy) { None } else { prompt.temperature }, + maximum_response_tokens: prompt.season, + sampling, + prompt: said, + }) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Availability { + Available, + Dormant(String), + Never(String), +} + +pub async fn probe(bridge: &Path) -> Result { + let run = tokio::process::Command::new(bridge).arg("--probe").kill_on_drop(true).output(); + let out = tokio::time::timeout(Duration::from_secs(30), run) + .await + .with_context(|| format!("{} --probe did not answer in 30s", bridge.display()))? + .with_context(|| format!("run {} --probe", bridge.display()))?; + let text = String::from_utf8_lossy(&out.stdout); + let heard = text.lines().rev().find_map(|line| serde_json::from_str::(line).ok()); + match heard { + Some(Said::Probe { available: true, .. }) => Ok(Availability::Available), + Some(Said::Probe { reason, .. }) => { + let reason = reason.unwrap_or_else(|| "unavailable".into()); + Ok(if reason == "device_not_eligible" { Availability::Never(reason) } else { Availability::Dormant(reason) }) + } + _ => bail!( + "{} --probe said no probe ({}): {}{}", + bridge.display(), + out.status, + text.trim(), + String::from_utf8_lossy(&out.stderr).trim() + ), + } +} + +pub struct Frost { + stops: Vec, + held: String, +} + +impl Frost { + pub fn new(stops: Option<&[String]>) -> Frost { + let stops = stops.unwrap_or(&[]).iter().filter(|s| !s.is_empty()).cloned().collect(); + Frost { stops, held: String::new() } + } + + pub fn pass(&mut self, text: &str) -> (String, bool) { + if self.stops.is_empty() { + return (text.to_string(), false); + } + self.held.push_str(text); + if let Some(at) = self.stops.iter().filter_map(|s| self.held.find(s.as_str())).min() { + let said = self.held[..at].to_string(); + self.held.clear(); + return (said, true); + } + let longest = self.stops.iter().map(String::len).max().unwrap_or(0); + let from = self.held.len().saturating_sub(longest.saturating_sub(1)); + let cut = self + .held + .char_indices() + .map(|(at, _)| at) + .filter(|at| *at >= from) + .find(|at| self.stops.iter().any(|s| s.starts_with(&self.held[*at..]))) + .unwrap_or(self.held.len()); + let said: String = self.held.drain(..cut).collect(); + (said, false) + } + + pub fn thaw(&mut self) -> String { + std::mem::take(&mut self.held) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::{json, Value}; + + fn prompt(v: Value) -> PromptBody { + serde_json::from_value(v).unwrap() + } + + #[test] + fn a_graft_becomes_the_turns_before_the_prompt() { + let p = prompt(json!({ + "chi": "prompt", + "systemPrompt": "be brief", + "content": [{"kind": "text", "text": "and now?"}], + "graft": [ + {"content": [{"kind": "text", "text": "hi"}], + "grew": [{"kind": "sepal", "text": "hm"}, {"kind": "text", "text": "hello "}, {"kind": "text", "text": "there"}]}, + {"content": [{"kind": "text", "text": "name a fruit"}], "grew": [{"kind": "text", "text": "apple"}]}, + {"content": [], "grew": []}, + ], + })); + let before = turns(p.graft.as_deref().unwrap()); + assert_eq!( + before, + [ + Turn { prompt: "hi".into(), response: "hello there".into() }, + Turn { prompt: "name a fruit".into(), response: "apple".into() }, + ], + "the words of each bloom, its thinking aside, and an empty bloom dropped" + ); + let ask = ask(&p, &before).unwrap(); + assert_eq!(ask.instructions.as_deref(), Some("be brief")); + assert_eq!(ask.prompt, "and now?"); + assert_eq!(ask.turns.len(), 2); + } + + #[test] + fn the_request_is_one_line_with_the_prompt_last() { + let p = prompt(json!({"text": "say \"hi\"\non two lines", "season": 32, "temperature": 0.4})); + let line = serde_json::to_string(&ask(&p, &[Turn { prompt: "a".into(), response: "b".into() }]).unwrap()).unwrap(); + assert!(!line.contains('\n')); + assert_eq!( + serde_json::from_str::(&line).unwrap(), + json!({"turns": [{"prompt": "a", "response": "b"}], "temperature": 0.4, "maximumResponseTokens": 32, "prompt": "say \"hi\"\non two lines"}) + ); + assert!(line.ends_with(r#""prompt":"say \"hi\"\non two lines"}"#), "{line}"); + } + + #[test] + fn how_a_bloom_grows_becomes_how_apple_samples() { + let s = |v: Value| sampling(&prompt(v)); + assert_eq!(s(json!({"temperature": 0.0, "prune": {"k": 5}})), Some(Sampling::Greedy)); + assert_eq!(s(json!({"prune": {"k": 40, "p": 0.9}, "seed": 7})), Some(Sampling::Top { k: 40, seed: Some(7) })); + assert_eq!(s(json!({"prune": {"p": 0.9}})), Some(Sampling::Nucleus { p: 0.9, seed: None })); + assert_eq!(s(json!({"seed": 3})), Some(Sampling::Nucleus { p: 1.0, seed: Some(3) })); + assert_eq!(s(json!({"temperature": 0.7})), None); + + let greedy = ask(&prompt(json!({"text": "x", "temperature": 0.0})), &[]).unwrap(); + assert_eq!(greedy.temperature, None, "greedy needs no temperature"); + assert_eq!(serde_json::to_value(&greedy.sampling).unwrap(), json!({"mode": "greedy"})); + let top = serde_json::to_value(Sampling::Top { k: 40, seed: None }).unwrap(); + assert_eq!(top, json!({"mode": "top", "k": 40, "seed": null})); + } + + #[test] + fn pollen_a_trellis_and_no_words_are_refused() { + let pollen = prompt(json!({"content": [ + {"kind": "text", "text": "what is this?"}, + {"kind": "pollen", "grain": {"kind": "image", "mediaType": "image/png", "data": "aGk="}}, + ]})); + assert_eq!(ask(&pollen, &[]).unwrap_err().code, "apple_fm_words_only"); + let trellis = prompt(json!({"text": "json please", "trellis": "json"})); + assert_eq!(ask(&trellis, &[]).unwrap_err().code, "apple_fm_no_trellis"); + assert_eq!(ask(&prompt(json!({"content": []})), &[]).unwrap_err().code, "apple_fm_no_words"); + } + + #[test] + fn what_the_bridge_says_is_read() { + let read = |s: &str| serde_json::from_str::(s).unwrap(); + assert_eq!(read(r#"{"text":"hi","event":"delta"}"#), Said::Delta { text: "hi".into() }); + assert_eq!( + read(r#"{"event":"finish","inputTokens":3,"outputTokens":2}"#), + Said::Finish { input_tokens: Some(3), output_tokens: Some(2) } + ); + assert_eq!(read(r#"{"event":"finish"}"#), Said::Finish { input_tokens: None, output_tokens: None }); + assert_eq!( + read(r#"{"event":"error","code":"refusal","message":"no"}"#), + Said::Error { code: "refusal".into(), message: "no".into() } + ); + assert_eq!( + read(r#"{"event":"probe","available":false,"reason":"model_not_ready"}"#), + Said::Probe { available: false, reason: Some("model_not_ready".into()) } + ); + } + + #[test] + fn frost_holds_back_what_may_be_a_stop_and_ends_at_one() { + let stops = ["THREE".to_string(), "\n\n".to_string()]; + let mut frost = Frost::new(Some(&stops)); + assert_eq!(frost.pass("one two TH"), ("one two ".into(), false), "TH may begin THREE"); + assert_eq!(frost.pass("REE four"), ("".into(), true), "a stop across two deltas is met"); + + let mut frost = Frost::new(Some(&stops)); + assert_eq!(frost.pass("TH"), ("".into(), false)); + assert_eq!(frost.pass("ROW"), ("THROW".into(), false), "what turned out not to be a stop is said"); + assert_eq!(frost.pass("n\n"), ("n".into(), false)); + assert_eq!(frost.pass("x"), ("\nx".into(), false)); + assert_eq!(frost.pass("é\n"), ("é".into(), false), "held back on a char boundary"); + assert_eq!(frost.thaw(), "\n", "and what is held is said when the bloom finishes"); + + let mut none = Frost::new(None); + assert_eq!(none.pass("THREE"), ("THREE".into(), false)); + let mut empty = Frost::new(Some(&["".to_string()])); + assert_eq!(empty.pass("x"), ("x".into(), false), "an empty stop stops nothing"); + } +} diff --git a/hives/apple-fm/src/main.rs b/hives/apple-fm/src/main.rs new file mode 100644 index 00000000..f1c17607 --- /dev/null +++ b/hives/apple-fm/src/main.rs @@ -0,0 +1,86 @@ +//! apple-fm-nurse — Apple's on-device foundation model, as a nurse. +//! +//! It grows the model it advertises through `apple-fm-gen`, the Swift bridge +//! to FoundationModels beside it, and needs no network and no key. Before it +//! nestles it asks the bridge whether the model can run here. While Apple +//! Intelligence is off, or its model is still downloading, it waits without +//! nestling and asks again; on a Mac that cannot run the model at all it +//! says so and exits 0, so its unit stays down. A bridge that cannot be run +//! at all exits non-zero. +//! +//! Environment, an empty value being no value: +//! APPLE_FM_MODEL — the model id it advertises (default: apple-3b-on-device) +//! APPLE_FM_LABEL — the label its bee is entered as (default: main) +//! APPLE_FM_BRIDGE — the bridge (default: apple-fm-gen beside this binary) +//! APPLE_FM_PROBE_MS — how often a waiting nurse asks again (default: 60000) + +mod bridge; +mod nurse; + +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Result; +use hum_nest::{serve_nurse, NurseAdvert}; +use tracing::{info, warn}; + +use crate::bridge::Availability; +use crate::nurse::AppleFm; + +const HIVE: &str = "apple-fm"; + +fn var(name: &str) -> Option { + std::env::var(name).ok().filter(|v| !v.trim().is_empty()) +} + +fn beside_me() -> PathBuf { + std::env::current_exe() + .ok() + .and_then(|exe| exe.parent().map(|dir| dir.join("apple-fm-gen"))) + .unwrap_or_else(|| PathBuf::from("apple-fm-gen")) +} + +#[tokio::main] +async fn main() -> Result<()> { + hum_paths::init(); + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + let model = var("APPLE_FM_MODEL").unwrap_or_else(|| "apple-3b-on-device".into()); + let label = var("APPLE_FM_LABEL").unwrap_or_else(|| "main".into()); + let bridge = var("APPLE_FM_BRIDGE").map(PathBuf::from).unwrap_or_else(beside_me); + let every = Duration::from_millis(var("APPLE_FM_PROBE_MS").and_then(|ms| ms.trim().parse().ok()).unwrap_or(60_000)); + + let mut waiting: Option = None; + loop { + match bridge::probe(&bridge).await? { + Availability::Available => break, + Availability::Never(reason) => { + warn!(%reason, bridge = %bridge.display(), "apple-fm.never: this Mac cannot run Apple's on-device model, so this unit stays down"); + return Ok(()); + } + Availability::Dormant(reason) => { + if waiting.as_deref() != Some(reason.as_str()) { + warn!(%reason, every_ms = every.as_millis() as u64, "apple-fm.dormant: Apple's on-device model is not ready, asking again"); + } + waiting = Some(reason); + tokio::time::sleep(every).await; + } + } + } + info!(bridge = %bridge.display(), %model, "apple-fm.available"); + + let advert = NurseAdvert { + hive: HIVE.to_string(), + label, + version: env!("CARGO_PKG_VERSION").to_string(), + models: vec![model], + source: Some("https://github.com/adiled/hum/tree/main/hives/apple-fm".to_string()), + }; + serve_nurse(Arc::new(AppleFm { bridge }), advert).await +} diff --git a/hives/apple-fm/src/nurse.rs b/hives/apple-fm/src/nurse.rs new file mode 100644 index 00000000..4fdc855f --- /dev/null +++ b/hives/apple-fm/src/nurse.rs @@ -0,0 +1,474 @@ +//! The nurse that grows blooms through Apple's on-device foundation model. +//! +//! Each prompt runs the bridge once: it is asked the prompt as one JSON line +//! and streams back what the model grows, and the words it grows unfurl as a +//! text petal. The bloom wilts `stop` when the bridge finishes, with +//! `max_tokens` as the reason when the prompt's `season` ran out, and its +//! counts as nectar when Apple gives them. A bloom that meets one of its +//! `frost` stops there: what came before the stop unfurls, and the bridge is +//! killed. The bridge's own errors, and a bridge that ends without a word, +//! become `error`s. +//! +//! A cell lives for its sigil and remembers what it grew. A prompt that +//! carries a graft is grown from that graft; one that carries none, as a +//! forager that sends only the new words does, is grown from the turns the +//! cell has grown so far. So a sigil holds its conversation either way. +//! +//! A still kills the bridge mid-bloom and wilts the bloom `cancel`, and a +//! prompt still waiting is told it was not grown. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::sync::Arc; + +use anyhow::Result; +use async_trait::async_trait; +use serde_json::{json, Value}; +use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; +use tokio::process::Command; +use tokio::sync::{mpsc, oneshot, Mutex}; +use tokio_util::sync::CancellationToken; +use tracing::{info, warn}; + +use hum_nest::{Cell, Egg, Nurse, Propensity}; +use thrum_core::{PetalKind, PromptBody}; + +use crate::bridge::{self, Frost, Said, Turn}; + +pub struct AppleFm { + pub bridge: PathBuf, +} + +#[async_trait] +impl Nurse for AppleFm { + fn ephemeral(&self) -> bool { + false + } + + fn propensity(&self) -> Propensity { + Propensity::StatefulSession + } + + async fn raise(&self, egg: Egg) -> Result { + info!(sid = %egg.sid, model = %egg.model_id, "apple-fm.raise"); + let bridge = self.bridge.clone(); + let (tx_feed, mut rx_feed) = mpsc::channel::(64); + let (tx_mmm, rx_mmm) = mpsc::channel::(256); + let (tx_exit, rx_exit) = oneshot::channel::(); + let silence = CancellationToken::new(); + let stilled = silence.clone(); + + tokio::spawn(async move { + let mut grew: Vec = Vec::new(); + loop { + let tone = tokio::select! { + biased; + _ = stilled.cancelled() => break, + tone = rx_feed.recv() => match tone { + Some(tone) => tone, + None => break, + }, + }; + if tone.get("chi").and_then(Value::as_str) != Some("prompt") { + continue; + } + let prompt: PromptBody = serde_json::from_value(tone).unwrap_or_default(); + let mut before = match prompt.graft.as_deref() { + Some(graft) if !graft.is_empty() => bridge::turns(graft), + _ => grew.clone(), + }; + if let Some(turn) = grow(&bridge, &prompt, &before, &tx_mmm, &stilled).await { + before.push(turn); + grew = before; + } + } + rx_feed.close(); + while let Ok(tone) = rx_feed.try_recv() { + if tone.get("chi").and_then(Value::as_str) == Some("prompt") { + let _ = tx_mmm.send(fail("apple_fm_cell_stilled", "the cell was stilled before it grew this prompt".into())).await; + } + } + let _ = tx_exit.send(0); + }); + + Ok(Cell { + mark: None, + feed: tx_feed, + mmm: Arc::new(Mutex::new(rx_mmm)), + emerged: rx_exit, + ephemeral: false, + silence, + }) + } +} + +fn fail(code: &str, message: String) -> Value { + json!({ "chi": "error", "code": code, "message": message }) +} + +struct Unfurler { + tx: mpsc::Sender, + petals: BTreeMap<&'static str, u64>, +} + +impl Unfurler { + async fn grow(&mut self, kind: PetalKind, text: &str) { + if text.is_empty() { + return; + } + let next = self.petals.len() as u64; + let petal = *self.petals.entry(kind.as_str()).or_insert(next); + let _ = self.tx.send(json!({ "chi": "unfurl", "petal": petal, "kind": kind.as_str(), "text": text })).await; + } + + async fn close(&mut self) { + for (kind, petal) in std::mem::take(&mut self.petals) { + let _ = self.tx.send(json!({ "chi": "unfurl", "petal": petal, "kind": kind, "closed": true })).await; + } + } +} + +fn tail(text: &str) -> &str { + let text = text.trim(); + let from = text.len().saturating_sub(2000); + let from = (from..=text.len()).find(|at| text.is_char_boundary(*at)).unwrap_or(text.len()); + &text[from..] +} + +async fn grow( + bridge: &Path, + prompt: &PromptBody, + before: &[Turn], + tx: &mpsc::Sender, + stilled: &CancellationToken, +) -> Option { + let ask = match bridge::ask(prompt, before) { + Ok(ask) => ask, + Err(refusal) => { + let _ = tx.send(fail(refusal.code, refusal.message)).await; + return None; + } + }; + let mut child = match Command::new(bridge) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .kill_on_drop(true) + .spawn() + { + Ok(child) => child, + Err(e) => { + let _ = tx.send(fail("apple_fm_bridge_spawn", format!("could not run the bridge at {}: {e}", bridge.display()))).await; + return None; + } + }; + if let Some(mut stdin) = child.stdin.take() { + let mut line = serde_json::to_vec(&ask).unwrap_or_default(); + line.push(b'\n'); + if let Err(e) = stdin.write_all(&line).await { + warn!(err = %e, "apple-fm.bridge.stdin"); + } + } + let complaint = child.stderr.take().map(|mut stderr| { + tokio::spawn(async move { + let mut text = String::new(); + let _ = stderr.read_to_string(&mut text).await; + text + }) + }); + let Some(stdout) = child.stdout.take() else { + let _ = tx.send(fail("apple_fm_bridge_spawn", "the bridge has no stdout".into())).await; + return None; + }; + let mut lines = BufReader::new(stdout).lines(); + let mut unfurler = Unfurler { tx: tx.clone(), petals: BTreeMap::new() }; + let mut frost = Frost::new(prompt.frost.as_deref()); + let mut said = String::new(); + let turn = |said: String| Turn { prompt: ask.prompt.clone(), response: said }; + + loop { + let line = tokio::select! { + biased; + _ = stilled.cancelled() => { + let _ = child.kill().await; + unfurler.close().await; + let _ = tx.send(json!({ "chi": "wilt", "reason": "cancel" })).await; + return None; + } + line = lines.next_line() => line, + }; + let line = match line { + Ok(Some(line)) => line, + Ok(None) => break, + Err(e) => { + warn!(err = %e, "apple-fm.bridge.stdout"); + break; + } + }; + let heard = match serde_json::from_str::(&line) { + Ok(heard) => heard, + Err(e) => { + warn!(err = %e, line = %line.chars().take(200).collect::(), "apple-fm.bridge.unread"); + continue; + } + }; + match heard { + Said::Delta { text } => { + let (free, met) = frost.pass(&text); + said.push_str(&free); + unfurler.grow(PetalKind::Text, &free).await; + if met { + let _ = child.kill().await; + unfurler.close().await; + let _ = tx.send(json!({ "chi": "wilt", "reason": "stop" })).await; + return Some(turn(said)); + } + } + Said::Finish { input_tokens, output_tokens } => { + let thawed = frost.thaw(); + said.push_str(&thawed); + unfurler.grow(PetalKind::Text, &thawed).await; + unfurler.close().await; + let ran_out = matches!((prompt.season, output_tokens), (Some(season), Some(out)) if out >= season); + let mut wilt = json!({ "chi": "wilt", "reason": if ran_out { "max_tokens" } else { "stop" } }); + if input_tokens.is_some() || output_tokens.is_some() { + wilt["nectar"] = json!({ + "input_tokens": input_tokens.unwrap_or(0), + "output_tokens": output_tokens.unwrap_or(0), + }); + } + let _ = tx.send(wilt).await; + let _ = child.wait().await; + return Some(turn(said)); + } + Said::Error { code, message } => { + unfurler.close().await; + let _ = tx.send(fail(&format!("apple_fm_{code}"), message)).await; + let _ = child.wait().await; + return None; + } + Said::Probe { .. } => {} + } + } + + let status = child.wait().await.map(|s| s.to_string()).unwrap_or_else(|e| e.to_string()); + let complaint = match complaint { + Some(task) => task.await.unwrap_or_default(), + None => String::new(), + }; + unfurler.close().await; + let _ = tx + .send(fail("apple_fm_bridge_died", format!("the bridge ended before it finished ({status}): {}", tail(&complaint)))) + .await; + None +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + const STUB: &str = r#"#!/bin/sh +here="$(cd "$(dirname "$0")" && pwd)" +IFS= read -r line +printf '%s\n' "$line" >> "$here/asked" +echo $$ > "$here/pid" +words=$(printf '%s' "$line" | sed -n 's/.*"prompt":"\([^"]*\)"}$/\1/p') +case "$words" in + boom*) echo '{"event":"error","code":"guardrail_violation","message":"the guardrail held"}'; exit 1 ;; + die*) echo 'the bridge fell over' >&2; exit 3 ;; + hang*) echo '{"event":"delta","text":"hanging"}'; exec sleep 30 ;; + frost*) echo '{"event":"delta","text":"one two TH"}'; echo '{"event":"delta","text":"REE four"}'; exec sleep 30 ;; + long*) echo '{"event":"delta","text":"on and on"}'; echo '{"event":"finish","inputTokens":4,"outputTokens":8}'; exit 0 ;; +esac +echo '{"event":"delta","text":"heard "}' +printf '{"event":"delta","text":"%s. "}\n' "$words" +echo '{"event":"finish","inputTokens":7,"outputTokens":3}' +"#; + + struct Stub { + dir: tempfile::TempDir, + } + + impl Stub { + fn new() -> Stub { + use std::os::unix::fs::PermissionsExt; + let dir = tempfile::tempdir().unwrap(); + let bridge = dir.path().join("apple-fm-gen"); + std::fs::write(&bridge, STUB).unwrap(); + std::fs::set_permissions(&bridge, std::fs::Permissions::from_mode(0o755)).unwrap(); + Stub { dir } + } + + fn nurse(&self) -> AppleFm { + AppleFm { bridge: self.dir.path().join("apple-fm-gen") } + } + + fn asked(&self) -> Vec { + std::fs::read_to_string(self.dir.path().join("asked")) + .unwrap_or_default() + .lines() + .map(|l| serde_json::from_str(l).unwrap()) + .collect() + } + + fn alive(&self) -> bool { + let pid = std::fs::read_to_string(self.dir.path().join("pid")).unwrap(); + std::process::Command::new("kill").args(["-0", pid.trim()]).stderr(std::process::Stdio::null()).status().unwrap().success() + } + } + + fn fed(words: &str) -> Value { + json!({"chi": "prompt", "sid": "s", "content": [{"kind": "text", "text": words}]}) + } + + async fn raised(stub: &Stub) -> Cell { + let egg = Egg::new(hum_identity::HumId::mint(), "apple-3b-on-device", "/tmp"); + stub.nurse().raise(egg).await.unwrap() + } + + async fn closing(cell: &Cell, blooms: usize) -> Vec { + let mut mmm = cell.mmm.lock().await; + let mut heard = Vec::new(); + while heard.iter().filter(|t: &&Value| t["chi"] == "wilt" || t["chi"] == "error").count() < blooms { + let tone = tokio::time::timeout(Duration::from_secs(10), mmm.recv()).await; + heard.push(tone.expect("the cell answers").expect("the cell lives on")); + } + heard + } + + fn said(tones: &[Value]) -> String { + tones.iter().filter(|t| t["chi"] == "unfurl").filter_map(|t| t["text"].as_str()).collect() + } + + fn ends(tones: &[Value]) -> Vec { + tones.iter().filter(|t| t["chi"] == "wilt" || t["chi"] == "error").cloned().collect() + } + + #[tokio::test] + async fn a_prompt_unfurls_and_wilts_with_its_nectar() { + let stub = Stub::new(); + let cell = raised(&stub).await; + cell.feed.send(fed("hello")).await.unwrap(); + let tones = closing(&cell, 1).await; + assert_eq!(said(&tones), "heard hello. "); + assert_eq!( + tones.iter().filter(|t| t["chi"] == "unfurl").map(|t| (t["petal"].clone(), t["closed"].clone())).collect::>(), + [(json!(0), Value::Null), (json!(0), Value::Null), (json!(0), json!(true))], + "one text petal, closed before the wilt" + ); + assert_eq!(ends(&tones), [json!({"chi": "wilt", "reason": "stop", "nectar": {"input_tokens": 7, "output_tokens": 3}})]); + } + + #[tokio::test] + async fn a_cell_remembers_its_turns_and_a_graft_overrides_them() { + let stub = Stub::new(); + let cell = raised(&stub).await; + let mut first = fed("one"); + first["systemPrompt"] = json!("be brief"); + cell.feed.send(first).await.unwrap(); + cell.feed.send(fed("two")).await.unwrap(); + assert_eq!(said(&closing(&cell, 2).await), "heard one. heard two. "); + let graft = json!([{"content": [{"kind": "text", "text": "elsewhere"}], "grew": [{"kind": "text", "text": "then"}]}]); + let mut grafted = fed("three"); + grafted["graft"] = graft; + cell.feed.send(grafted).await.unwrap(); + cell.feed.send(fed("four")).await.unwrap(); + closing(&cell, 2).await; + + let asked = stub.asked(); + assert_eq!(asked[0], json!({"instructions": "be brief", "turns": [], "prompt": "one"})); + assert_eq!(asked[1]["turns"], json!([{"prompt": "one", "response": "heard one. "}]), "the cell's own turn"); + assert_eq!(asked[2]["turns"], json!([{"prompt": "elsewhere", "response": "then"}]), "the graft, not the cell's turns"); + assert_eq!( + asked[3]["turns"], + json!([{"prompt": "elsewhere", "response": "then"}, {"prompt": "three", "response": "heard three. "}]), + "and the cell grows on from the graft" + ); + } + + #[tokio::test] + async fn a_still_kills_the_bridge_and_closes_what_it_was_fed() { + let stub = Stub::new(); + let cell = raised(&stub).await; + cell.feed.send(fed("hang on")).await.unwrap(); + cell.feed.send(fed("waiting")).await.unwrap(); + { + let mut mmm = cell.mmm.lock().await; + let first = tokio::time::timeout(Duration::from_secs(10), mmm.recv()).await.unwrap().unwrap(); + assert_eq!(first["text"], "hanging"); + } + assert!(stub.alive()); + cell.still(); + let tones = closing(&cell, 2).await; + let ends: Vec = ends(&tones) + .iter() + .map(|t| format!("{} {}", t["chi"].as_str().unwrap(), t["reason"].as_str().or(t["code"].as_str()).unwrap())) + .collect(); + assert_eq!(ends, ["wilt cancel", "error apple_fm_cell_stilled"], "the growing bloom wilts, the waiting one is told"); + assert!(!stub.alive(), "the bridge is killed, not left to grow"); + assert!(cell.mmm.lock().await.recv().await.is_none(), "then the cell ends"); + assert_eq!(cell.emerged.await, Ok(0)); + } + + #[tokio::test] + async fn a_stop_ends_the_bloom_before_it_and_kills_the_bridge() { + let stub = Stub::new(); + let cell = raised(&stub).await; + let mut frosted = fed("frost"); + frosted["frost"] = json!(["THREE"]); + cell.feed.send(frosted).await.unwrap(); + let tones = closing(&cell, 1).await; + assert_eq!(said(&tones), "one two "); + assert_eq!(ends(&tones), [json!({"chi": "wilt", "reason": "stop"})]); + assert!(!stub.alive()); + cell.feed.send(fed("after")).await.unwrap(); + closing(&cell, 1).await; + assert_eq!(stub.asked()[1]["turns"], json!([{"prompt": "frost", "response": "one two "}]), "the turn is what was said"); + } + + #[tokio::test] + async fn a_season_that_ran_out_wilts_max_tokens() { + let stub = Stub::new(); + let cell = raised(&stub).await; + let mut seasoned = fed("long"); + seasoned["season"] = json!(8); + cell.feed.send(seasoned).await.unwrap(); + let tones = closing(&cell, 1).await; + assert_eq!(ends(&tones)[0]["reason"], "max_tokens"); + assert_eq!(stub.asked()[0]["maximumResponseTokens"], 8); + let mut roomy = fed("long"); + roomy["season"] = json!(9); + cell.feed.send(roomy).await.unwrap(); + assert_eq!(ends(&closing(&cell, 1).await)[0]["reason"], "stop"); + } + + #[tokio::test] + async fn what_goes_wrong_is_told_and_the_cell_grows_on() { + let stub = Stub::new(); + let cell = raised(&stub).await; + cell.feed.send(fed("boom")).await.unwrap(); + cell.feed.send(fed("die now")).await.unwrap(); + cell.feed.send(json!({"chi": "prompt", "sid": "s", "text": "look", "trellis": "json"})).await.unwrap(); + cell.feed.send(fed("fine")).await.unwrap(); + let tones = closing(&cell, 4).await; + let ends = ends(&tones); + assert_eq!(ends[0], json!({"chi": "error", "code": "apple_fm_guardrail_violation", "message": "the guardrail held"})); + assert_eq!(ends[1]["code"], "apple_fm_bridge_died"); + let died = ends[1]["message"].as_str().unwrap(); + assert!(died.contains("exit status: 3") && died.contains("the bridge fell over"), "{died}"); + assert_eq!(ends[2]["code"], "apple_fm_no_trellis"); + assert_eq!(ends[3]["reason"], "stop"); + assert_eq!(stub.asked().len(), 3, "the refused prompt never ran the bridge"); + assert_eq!(stub.asked()[2]["turns"], json!([]), "a bloom that failed is no turn"); + } + + #[tokio::test] + async fn a_missing_bridge_is_told() { + let nurse = AppleFm { bridge: "/nonexistent/apple-fm-gen".into() }; + let cell = nurse.raise(Egg::new(hum_identity::HumId::mint(), "m", "/tmp")).await.unwrap(); + cell.feed.send(fed("hi")).await.unwrap(); + assert_eq!(ends(&closing(&cell, 1).await)[0]["code"], "apple_fm_bridge_spawn"); + } +} diff --git a/hives/apple-fm/tests/common/mod.rs b/hives/apple-fm/tests/common/mod.rs new file mode 100644 index 00000000..b6710f86 --- /dev/null +++ b/hives/apple-fm/tests/common/mod.rs @@ -0,0 +1,260 @@ +//! The world apple-fm's nurse grows in: a humd in a root of its own, the +//! nurse as the process orchd would run, a bridge that stands in for +//! Apple's, and a forager that prompts through humd. +//! +//! The stand-in bridge answers `heard . `, or by the first word: +//! `hang` unfurls once and waits to be killed. It answers `--probe` with +//! the line in its `probe` file, or as available when there is none, and +//! keeps each request it was asked in `asked`. + +#![allow(dead_code)] + +use std::path::{Path, PathBuf}; +use std::sync::OnceLock; +use std::time::{Duration, Instant}; + +use hum_thrum::{Bee, Event, Options}; +use serde_json::{json, Value}; +use thrum_core::Role; + +pub const WAIT: Duration = Duration::from_secs(30); + +pub struct World { + pub root: PathBuf, +} + +static WORLD: OnceLock = OnceLock::new(); + +pub fn world() -> &'static World { + WORLD.get_or_init(World::new) +} + +impl World { + fn new() -> World { + let root = tempfile::Builder::new().prefix("apple-fm").tempdir().unwrap().keep(); + unsafe { + std::env::set_var("XDG_STATE_HOME", root.join("s")); + std::env::set_var("XDG_CONFIG_HOME", root.join("c")); + std::env::set_var("XDG_DATA_HOME", root.join("d")); + std::env::set_var("XDG_CACHE_HOME", root.join("k")); + std::env::set_var("XDG_RUNTIME_DIR", root.join("r")); + std::env::set_var("HUM_LOG_LEVEL", "warn"); + std::env::remove_var("HUM_THRUM_SOCK"); + std::env::remove_var("HUM_SOCKET"); + } + std::fs::create_dir_all(hum_paths::hives_d_dir()).unwrap(); + std::fs::write(hum_paths::hum_json(), json!({"humd": {"metricsAddr": "none", "irohPort": 0}}).to_string()).unwrap(); + for hive in ["apple-fm", "probe"] { + std::fs::write(hum_paths::hive_installation(hive), "# apple-fm's test\n").unwrap(); + } + let sock = hum_paths::thrum_sock_resolved(); + let _ = std::fs::remove_file(&sock); + std::thread::spawn(|| { + let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap(); + rt.block_on(async { + let mut cfg = humd::DaemonConfig::from_env(); + cfg.humd_key = None; + let _ = humd::run(cfg, std::future::pending::<()>()).await; + }); + }); + let since = Instant::now(); + while std::os::unix::net::UnixStream::connect(&sock).is_err() { + assert!(since.elapsed() < WAIT, "humd did not bind {}", sock.display()); + std::thread::sleep(Duration::from_millis(50)); + } + World { root } + } +} + +const STUB: &str = r#"#!/bin/sh +here="$(cd "$(dirname "$0")" && pwd)" +if [ "$1" = "--probe" ]; then + if [ -f "$here/probe" ]; then cat "$here/probe"; else echo '{"event":"probe","available":true}'; fi + exit 0 +fi +IFS= read -r line +printf '%s\n' "$line" >> "$here/asked" +echo $$ > "$here/pid" +words=$(printf '%s' "$line" | sed -n 's/.*"prompt":"\([^"]*\)"}$/\1/p') +case "$words" in + hang*) echo '{"event":"delta","text":"hanging"}'; exec sleep 30 ;; +esac +echo '{"event":"delta","text":"heard "}' +printf '{"event":"delta","text":"%s. "}\n' "$words" +echo '{"event":"finish","inputTokens":7,"outputTokens":3}' +"#; + +pub struct Stub { + dir: tempfile::TempDir, +} + +impl Stub { + pub fn new() -> Stub { + use std::os::unix::fs::PermissionsExt; + let dir = tempfile::tempdir().unwrap(); + let bridge = dir.path().join("apple-fm-gen"); + std::fs::write(&bridge, STUB).unwrap(); + std::fs::set_permissions(&bridge, std::fs::Permissions::from_mode(0o755)).unwrap(); + Stub { dir } + } + + pub fn bridge(&self) -> PathBuf { + self.dir.path().join("apple-fm-gen") + } + + pub fn probe(&self, available: bool, reason: &str) { + let line = json!({"event": "probe", "available": available, "reason": reason}); + std::fs::write(self.dir.path().join("probe"), format!("{line}\n")).unwrap(); + } + + pub fn asked(&self) -> Vec { + std::fs::read_to_string(self.dir.path().join("asked")) + .unwrap_or_default() + .lines() + .map(|l| serde_json::from_str(l).unwrap()) + .collect() + } + + pub fn alive(&self) -> bool { + let pid = std::fs::read_to_string(self.dir.path().join("pid")).unwrap(); + std::process::Command::new("kill") + .args(["-0", pid.trim()]) + .stderr(std::process::Stdio::null()) + .status() + .unwrap() + .success() + } +} + +pub struct Nurse { + child: tokio::process::Child, + pub model: String, + pub log: PathBuf, +} + +impl Nurse { + pub fn spawn(label: &str, bridge: &Path, env: &[(&str, &str)]) -> Nurse { + let w = world(); + let model = format!("apple-fm-{label}"); + let log = w.root.join(format!("nurse-{label}.log")); + let out = std::fs::File::create(&log).unwrap(); + let mut cmd = tokio::process::Command::new(env!("CARGO_BIN_EXE_apple-fm-nurse")); + cmd.env_remove("APPLE_FM_PROBE_MS") + .env("APPLE_FM_LABEL", label) + .env("APPLE_FM_MODEL", &model) + .env("APPLE_FM_BRIDGE", bridge) + .env("RUST_LOG", "info") + .env("NO_COLOR", "1") + .stdout(out.try_clone().unwrap()) + .stderr(out) + .kill_on_drop(true); + for (k, v) in env { + cmd.env(k, v); + } + Nurse { child: cmd.spawn().unwrap(), model, log } + } + + pub fn log_text(&self) -> String { + std::fs::read_to_string(&self.log).unwrap_or_default() + } + + pub async fn wait_for(&self, line: &str) { + let since = Instant::now(); + while !self.log_text().contains(line) { + assert!(since.elapsed() < WAIT, "no {line:?} in the nurse's log:\n{}", self.log_text()); + tokio::time::sleep(Duration::from_millis(25)).await; + } + } + + pub async fn nestled(self) -> Nurse { + self.wait_for("nurse.nestled").await; + self + } + + pub async fn exit(&mut self) -> std::process::ExitStatus { + tokio::time::timeout(WAIT, self.child.wait()).await.expect("the nurse exits").unwrap() + } +} + +pub struct Forager { + bee: Bee, +} + +impl Forager { + pub async fn link(label: &str) -> Forager { + world(); + let bee = Bee::link(Options::new("probe", label, Role::Forager).hello(json!({"version": "1.0.0"}))).await.unwrap(); + Forager { bee } + } + + pub async fn breathed(&mut self, model: &str) { + let since = Instant::now(); + loop { + assert!(since.elapsed() < WAIT, "humd never breathed that a nurse grows {model}"); + let tone = self.next().await; + if tone["chi"] == "breath" && tone["models"].as_array().is_some_and(|m| m.iter().any(|m| m["id"] == model)) { + return; + } + } + } + + pub fn send(&self, tone: Value) { + self.bee.send(tone); + } + + pub fn prompt(&self, sid: &str, model: &str, words: &str) { + self.send(json!({"chi": "prompt", "sid": sid, "modelId": model, "content": [{"kind": "text", "text": words}]})); + } + + pub async fn next(&mut self) -> Value { + loop { + match tokio::time::timeout(WAIT, self.bee.next()).await.expect("a tone in time").expect("the link lives") { + Event::Tone(tone) => return tone, + _ => continue, + } + } + } + + pub async fn on(&mut self, sid: &str) -> Value { + loop { + let tone = self.next().await; + if tone["sid"] == sid { + return tone; + } + } + } + + pub async fn bloom(&mut self, sid: &str) -> Bloom { + let mut bloom = Bloom::default(); + loop { + let tone = self.on(sid).await; + match tone["chi"].as_str() { + Some("unfurl") => { + if let Some(text) = tone["text"].as_str() { + bloom.said.push_str(text); + bloom.unfurls += 1; + } + } + Some("wilt") | Some("error") => { + bloom.end = tone; + return bloom; + } + _ => {} + } + } + } +} + +#[derive(Debug, Default)] +pub struct Bloom { + pub said: String, + pub unfurls: usize, + pub end: Value, +} + +impl Bloom { + pub fn ended(&self) -> String { + let end = &self.end; + format!("{} {}", end["chi"].as_str().unwrap_or(""), end["reason"].as_str().or(end["code"].as_str()).unwrap_or("")) + } +} diff --git a/hives/apple-fm/tests/live.rs b/hives/apple-fm/tests/live.rs new file mode 100644 index 00000000..f344e924 --- /dev/null +++ b/hives/apple-fm/tests/live.rs @@ -0,0 +1,81 @@ +//! The hive whole against Apple's own model: the nurse, the bridge that +//! `./build` makes, and a humd between them and a forager. +//! +//! ./build && cargo test --test live -- --ignored +//! +//! APPLE_FM_LIVE_BRIDGE names another bridge than target/release/apple-fm-gen. + +mod common; + +use std::path::PathBuf; +use std::time::{Duration, Instant}; + +use common::*; +use serde_json::{json, Value}; + +fn bridge() -> PathBuf { + let bridge = std::env::var("APPLE_FM_LIVE_BRIDGE") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/release/apple-fm-gen")); + assert!(bridge.is_file(), "no bridge at {}; run ./build", bridge.display()); + bridge +} + +fn ask(sid: &str, model: &str, words: &str, more: Value) -> Value { + let mut tone = json!({"chi": "prompt", "sid": sid, "modelId": model, "content": [{"kind": "text", "text": words}]}); + for (k, v) in more.as_object().unwrap() { + tone[k] = v.clone(); + } + tone +} + +#[tokio::test(flavor = "multi_thread")] +#[ignore = "needs macOS 26 with Apple Intelligence on, and the bridge ./build makes"] +async fn a_conversation_grows_through_apple_s_model() { + let nurse = Nurse::spawn("live", &bridge(), &[]); + let since = Instant::now(); + while !nurse.log_text().contains("nurse.nestled") { + let log = nurse.log_text(); + assert!(!log.contains("apple-fm.dormant") && !log.contains("apple-fm.never"), "Apple's model cannot run here:\n{log}"); + assert!(since.elapsed() < WAIT, "the nurse did not nestle:\n{log}"); + tokio::time::sleep(Duration::from_millis(50)).await; + } + let m = nurse.model.clone(); + let mut f = Forager::link("live").await; + f.breathed(&m).await; + + f.send(ask("talk", &m, "Reply with exactly one word: apple", json!({"temperature": 0.0}))); + let first = f.bloom("talk").await; + assert_eq!(first.ended(), "wilt stop", "{first:?}"); + assert!(first.said.to_lowercase().contains("apple"), "{first:?}"); + assert!(first.end["nectar"]["output_tokens"].as_u64().unwrap() > 0, "{first:?}"); + + f.send(ask("talk", &m, "Which fruit did you just name? Reply with that one word.", json!({"temperature": 0.0}))); + let second = f.bloom("talk").await; + assert!(second.said.to_lowercase().contains("apple"), "the cell kept the conversation through humd: {second:?}"); + + let count = "Count from 1 to 30, separated by commas, and nothing else."; + f.send(ask("count", &m, count, json!({"temperature": 0.0}))); + let counted = f.bloom("count").await; + assert!(counted.unfurls > 1, "the bloom streams: {counted:?}"); + assert_eq!(counted.said.matches("1, 2, 3,").count(), 1, "each unfurl is new words, not the bloom so far: {counted:?}"); + assert!(counted.said.contains("29, 30"), "{counted:?}"); + + f.send(ask("frost", &m, count, json!({"temperature": 0.0, "frost": [", 10"]}))); + let frosted = f.bloom("frost").await; + assert_eq!(frosted.ended(), "wilt stop", "{frosted:?}"); + assert!(frosted.said.trim_end().ends_with('9') && !frosted.said.contains("10"), "the bloom ends before its stop: {frosted:?}"); + + f.send(ask("season", &m, "Write a long story about a bee.", json!({"temperature": 0.0, "season": 12}))); + let seasoned = f.bloom("season").await; + assert_eq!(seasoned.ended(), "wilt max_tokens", "{seasoned:?}"); + assert_eq!(seasoned.end["nectar"]["output_tokens"], 12, "{seasoned:?}"); + + f.send(ask("cancel", &m, "Write a 2000 word essay about the history of beekeeping.", json!({}))); + let first = f.on("cancel").await; + assert_eq!(first["chi"], "unfurl", "{first}"); + let since = Instant::now(); + f.send(json!({"chi": "cancel", "sid": "cancel"})); + assert_eq!(f.bloom("cancel").await.ended(), "wilt cancel"); + assert!(since.elapsed() < Duration::from_secs(2), "a cancel ends the bloom at once: {:?}", since.elapsed()); +} diff --git a/hives/apple-fm/tests/nurse.rs b/hives/apple-fm/tests/nurse.rs new file mode 100644 index 00000000..379d3e97 --- /dev/null +++ b/hives/apple-fm/tests/nurse.rs @@ -0,0 +1,94 @@ +//! The nurse as humd reaches it: a forager's prompt goes through humd to +//! the nurse and its bloom comes back, a sigil keeps its conversation, a +//! cancel kills the bridge, and a nurse whose Mac cannot grow yet waits +//! before it nestles. + +mod common; + +use std::time::Duration; + +use common::*; +use serde_json::json; + +#[tokio::test(flavor = "multi_thread")] +async fn a_prompt_through_humd_unfurls_and_wilts_back() { + let stub = Stub::new(); + let nurse = Nurse::spawn("grow", &stub.bridge(), &[]).nestled().await; + let mut f = Forager::link("grow").await; + f.breathed(&nurse.model).await; + + f.send(json!({"chi": "prompt", "sid": "a", "modelId": nurse.model, "systemPrompt": "be brief", + "content": [{"kind": "text", "text": "hello"}]})); + let bloom = f.bloom("a").await; + assert_eq!((bloom.said.as_str(), bloom.unfurls), ("heard hello. ", 2), "each delta unfurls as it comes"); + assert_eq!(bloom.end["reason"], "stop"); + assert_eq!(bloom.end["nectar"], json!({"input_tokens": 7, "output_tokens": 3})); + + f.prompt("a", &nurse.model, "again"); + assert_eq!(f.bloom("a").await.said, "heard again. "); + let asked = stub.asked(); + assert_eq!(asked[0]["instructions"], "be brief"); + assert_eq!( + asked[1]["turns"], + json!([{"prompt": "hello", "response": "heard hello. "}]), + "the sigil's next prompt, sent with no graft, grows on from what its cell grew" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_cancel_through_humd_kills_the_bridge() { + let stub = Stub::new(); + let nurse = Nurse::spawn("cancel", &stub.bridge(), &[]).nestled().await; + let mut f = Forager::link("cancel").await; + f.breathed(&nurse.model).await; + + f.prompt("c", &nurse.model, "hang on"); + let first = f.on("c").await; + assert_eq!((first["chi"].clone(), first["text"].clone()), (json!("unfurl"), json!("hanging"))); + assert!(stub.alive()); + f.send(json!({"chi": "cancel", "sid": "c"})); + assert_eq!(f.bloom("c").await.ended(), "wilt cancel"); + tokio::time::sleep(Duration::from_millis(200)).await; + assert!(!stub.alive(), "the bridge is killed with its bloom"); + + f.prompt("c", &nurse.model, "after"); + assert_eq!(f.bloom("c").await.said, "heard after. ", "the sigil grows on in a fresh cell"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_nurse_waits_unnestled_until_apple_s_model_is_ready() { + let stub = Stub::new(); + stub.probe(false, "model_not_ready"); + let nurse = Nurse::spawn("dormant", &stub.bridge(), &[("APPLE_FM_PROBE_MS", "100")]); + nurse.wait_for("apple-fm.dormant").await; + tokio::time::sleep(Duration::from_millis(600)).await; + let log = nurse.log_text(); + assert!(!log.contains("nurse.nestled"), "a dormant nurse does not nestle:\n{log}"); + assert_eq!(log.matches("apple-fm.dormant").count(), 1, "it says so once, not at every probe:\n{log}"); + assert!(log.contains("reason=model_not_ready"), "{log}"); + + let mut f = Forager::link("dormant").await; + stub.probe(true, ""); + let nurse = nurse.nestled().await; + f.breathed(&nurse.model).await; + f.prompt("d", &nurse.model, "awake"); + assert_eq!(f.bloom("d").await.said, "heard awake. "); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_mac_that_cannot_run_the_model_keeps_its_unit_down() { + let stub = Stub::new(); + stub.probe(false, "device_not_eligible"); + let mut nurse = Nurse::spawn("never", &stub.bridge(), &[("APPLE_FM_PROBE_MS", "100")]); + assert!(nurse.exit().await.success(), "it exits 0, so orchd does not restart it:\n{}", nurse.log_text()); + let log = nurse.log_text(); + assert!(log.contains("apple-fm.never") && !log.contains("nurse.nestled"), "{log}"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_bridge_that_cannot_run_fails_the_unit() { + world(); + let mut nurse = Nurse::spawn("missing", std::path::Path::new("/nonexistent/apple-fm-gen"), &[]); + assert!(!nurse.exit().await.success(), "a missing bridge is an install gone wrong:\n{}", nurse.log_text()); + assert!(nurse.log_text().contains("/nonexistent/apple-fm-gen"), "{}", nurse.log_text()); +} diff --git a/release.sh b/release.sh index eabe2f0f..73502789 100755 --- a/release.sh +++ b/release.sh @@ -19,6 +19,7 @@ set -euo pipefail # what `hum hive install` publishes, so those track the same number. EXTRA_VERSIONED_FILES=( hives/anthropic-server/package.json + hives/apple-fm/Cargo.toml hives/bash/Cargo.toml hives/bp7/Cargo.toml hives/claude-cli/Cargo.toml From 6e81d0d6671f60579397b2c4c3a7666afb668d2f Mon Sep 17 00:00:00 2001 From: Adil Date: Sun, 1 Nov 2026 17:49:43 +0500 Subject: [PATCH 3/4] hive/apple-fm: a conversation longer than the model's context grows from its newest turns Apple's model has a 4096-token context, and a cell that remembers its sigil's turns would otherwise fail every prompt once the conversation outgrew it. The bridge now counts tokens (macOS 26.4+) and keeps the newest turns that leave room for the reply (its season, else 1024 tokens), reporting how many it dropped in its finish; the cell forgets the same turns. A reply with no season is held to the room left, so it wilts max_tokens instead of overflowing mid-stream, and the finish carries the bridge's own reason. Before 26.4 the bridge drops the older half of the turns whenever Apple refuses the transcript. The cell no longer waits on a bridge that lingers after its finish or error: it kills it. Tests cover the dropped turns, the bridge's reason, a lingering bridge, and, live, a conversation past the context. --- hives/apple-fm/README.md | 32 +++++++--- hives/apple-fm/bridge/gen.swift | 107 ++++++++++++++++++++++++-------- hives/apple-fm/src/bridge.rs | 14 +++-- hives/apple-fm/src/nurse.rs | 67 +++++++++++++++++--- hives/apple-fm/tests/live.rs | 15 +++++ 5 files changed, 185 insertions(+), 50 deletions(-) diff --git a/hives/apple-fm/README.md b/hives/apple-fm/README.md index b3e3cee0..516bc0b0 100644 --- a/hives/apple-fm/README.md +++ b/hives/apple-fm/README.md @@ -61,7 +61,10 @@ A bloom grows through one run of the bridge: a graft is grown from its graft. One that carries none, as from a forager that sends only a sigil's new words, is grown from the turns its cell has grown so far, so a sigil keeps its conversation either - way. + way. The model's context is 4096 tokens, so the bridge keeps the + newest turns that leave room for the reply (its `season`, else 1024 + tokens) and drops the older ones, and the cell forgets them too; the + nurse logs `apple-fm.trimmed`. - **How it grows.** `temperature` and `season` become Apple's `temperature` and `maximumResponseTokens`. A `temperature` of 0 samples greedily; `prune.k` samples from the top k, else `prune.p` @@ -72,15 +75,20 @@ A bloom grows through one run of the bridge: - **The reply.** Its words unfurl as a text petal as the model grows them, each unfurl only the words new since the last. The bloom wilts `stop`, or `max_tokens` when it grew all of its `season`, with Apple's - token counts as nectar (macOS 26.4 and later). A `cancel` kills the - bridge and wilts it `cancel`. + token counts as nectar. A reply with no `season` is held to the + context left after the conversation, so a long one wilts `max_tokens` + rather than overflowing. A `cancel` kills the bridge and wilts it + `cancel`. (Counts, and the room the reply is held to, need macOS 26.4 + or later; before it the bridge drops the older half of the turns + whenever Apple refuses a conversation as too long.) - **Refused.** Apple's model grows words from words, so a prompt whose petals carry pollen is refused with `apple_fm_words_only`, and one with a `trellis` with `apple_fm_no_trellis`. The nurse does not grow on: a prompt with no words is refused by its nest with `cannot-grow-on`, or `empty-prompt` when it carries no graft either. - **Errors.** Apple's own become `apple_fm_`: - `apple_fm_exceeded_context_window` past the model's 4096 tokens, + `apple_fm_exceeded_context_window` when the prompt alone overflows + the model's context, `apple_fm_guardrail_violation`, `apple_fm_refusal`, `apple_fm_unsupported_language`, `apple_fm_rate_limited`, `apple_fm_assets_unavailable`, and `apple_fm_unavailable` when the @@ -112,12 +120,13 @@ stdin {"instructions": "...", "turns": [{"prompt": "...", "response": "..."}], "sampling": {"mode": "greedy" | "top" | "nucleus", "k": 40, "p": 0.9, "seed": 7}, "prompt": "..."} stdout {"event": "delta", "text": "..."} - {"event": "finish", "inputTokens": 15, "outputTokens": 75} + {"event": "finish", "reason": "stop", "dropped": 2, "inputTokens": 15, "outputTokens": 75} {"event": "error", "code": "guardrail_violation", "message": "..."} ``` Apple streams each snapshot of the reply whole; the bridge writes only -what is new in it. `apple-fm-gen --probe` reads nothing and writes +what is new in it. `dropped` counts the oldest turns it left out to fit +the context. `apple-fm-gen --probe` reads nothing and writes `{"event": "probe", "available": true}`, or `false` with a `reason`: `apple_intelligence_not_enabled`, `model_not_ready` or `device_not_eligible`. @@ -164,9 +173,10 @@ unfurls and wilts back with its nectar, the sigil's next prompt grows on from what its cell grew, a cancel kills the bridge, a nurse waits unnestled until the model is ready and then grows, one on a Mac that cannot run the model exits 0, and one whose bridge cannot run fails. -Its cells' tests cover the turns a cell keeps and a graft that overrides -them, a still, a stop, a season that ran out, and each way a bridge can -fail; the unit tests cover the request a prompt becomes, its sampling, +Its cells' tests cover the turns a cell keeps, a graft that overrides +them and the turns the bridge dropped, a still, a stop, a season that +ran out, a bridge that lingers after it finished, and each way a bridge +can fail; the unit tests cover the request a prompt becomes, its sampling, what is refused, what the bridge says, and how `frost` holds back what may be a stop. @@ -177,7 +187,9 @@ Against Apple's own model, on a Mac that can run it: ``` It holds a conversation through humd, streams a count with each unfurl -new, stops at a stop, wilts `max_tokens` at its season, and cancels. +new, stops at a stop, wilts `max_tokens` at its season, grows a +conversation longer than the model's context from its newest turns, and +cancels. ## Use diff --git a/hives/apple-fm/bridge/gen.swift b/hives/apple-fm/bridge/gen.swift index a21cfc4d..d9e7b5e8 100644 --- a/hives/apple-fm/bridge/gen.swift +++ b/hives/apple-fm/bridge/gen.swift @@ -11,10 +11,18 @@ // and writes NDJSON on stdout, each line as soon as it is known: // // {"event": "delta", "text": "..."} the words new since the last line -// {"event": "finish", "inputTokens": n, "outputTokens": n} -// (counts on macOS 26.4 and later) +// {"event": "finish", "reason": "stop" | "max_tokens", "dropped": n, +// "inputTokens": n, "outputTokens": n} (reason and counts on macOS 26.4 and later) // {"event": "error", "code": "...", "message": "..."} // +// The model's context is small (4096 tokens), so the oldest turns that do not +// fit are dropped and counted in the finish's `dropped`: the newest turns are +// kept that leave room for the reply, its `maximumResponseTokens` or 1024 +// tokens, and a reply with no `maximumResponseTokens` is held to the room that +// is left, so it ends `max_tokens` rather than overflowing. Where Apple cannot +// count tokens, a transcript Apple refuses as too long is retried with the +// older half of its turns dropped. +// // `gen --probe` reads nothing and writes one line, // {"event": "probe", "available": true|false, "reason": "..."}. // @@ -54,6 +62,7 @@ struct Event: Encodable { var message: String? = nil var inputTokens: Int? = nil var outputTokens: Int? = nil + var dropped: Int? = nil } @main @@ -115,6 +124,32 @@ struct Gen { [.text(Transcript.TextSegment(content: s))] } + static func exchange(_ turn: Turn) -> [Transcript.Entry] { + [ + .prompt(Transcript.Prompt(segments: text(turn.prompt))), + .response(Transcript.Response(assetIDs: [], segments: text(turn.response))), + ] + } + + static let reply = 1024 + + @available(macOS 26.4, *) + static func fit(_ turns: [Turn], around fixed: [Transcript.Entry], reserving: Int?) async -> (kept: [Turn], cap: Int)? { + let model = SystemLanguageModel.default + let context = model.contextSize + let reserve = min(reserving ?? reply, context / 2) + guard var used = try? await model.tokenCount(for: fixed) else { return nil } + var kept = 0 + for turn in turns.reversed() { + guard let counted = try? await model.tokenCount(for: exchange(turn)) else { return nil } + let cost = max(0, counted - 1) + if used + cost + reserve > context { break } + used += cost + kept += 1 + } + return (Array(turns.suffix(kept)), max(1, context - used - 16)) + } + static func main() async { if CommandLine.arguments.dropFirst().contains("--probe") { let why = unavailable() @@ -130,49 +165,67 @@ struct Gen { fail("unavailable", why) } - var entries: [Transcript.Entry] = [] + var head: [Transcript.Entry] = [] if let instructions = request.instructions, !instructions.isEmpty { - entries.append(.instructions(Transcript.Instructions(segments: text(instructions), toolDefinitions: []))) + head.append(.instructions(Transcript.Instructions(segments: text(instructions), toolDefinitions: []))) } - for turn in request.turns ?? [] { - entries.append(.prompt(Transcript.Prompt(segments: text(turn.prompt)))) - entries.append(.response(Transcript.Response(assetIDs: [], segments: text(turn.response)))) + let asked: Transcript.Entry = .prompt(Transcript.Prompt(segments: text(request.prompt))) + var turns = request.turns ?? [] + var dropped = 0 + var cap = request.maximumResponseTokens + if #available(macOS 26.4, *), let fitted = await fit(turns, around: head + [asked], reserving: request.maximumResponseTokens) { + dropped = turns.count - fitted.kept.count + turns = fitted.kept + if cap == nil { cap = fitted.cap } } - let session = LanguageModelSession(model: .default, transcript: Transcript(entries: entries)) let options = GenerationOptions( sampling: sampling(request.sampling), temperature: request.temperature, - maximumResponseTokens: request.maximumResponseTokens + maximumResponseTokens: cap ) var said = "" var sent = 0 - do { - for try await snapshot in session.streamResponse(to: request.prompt, options: options) { - said = snapshot.content - let scalars = said.unicodeScalars - let count = scalars.count - if count > sent { - var delta = String.UnicodeScalarView() - delta.append(contentsOf: scalars.dropFirst(sent)) - say(Event(event: "delta", text: String(delta))) - sent = count + while true { + let session = LanguageModelSession(model: .default, transcript: Transcript(entries: head + turns.flatMap(exchange))) + do { + for try await snapshot in session.streamResponse(to: request.prompt, options: options) { + said = snapshot.content + let scalars = said.unicodeScalars + let count = scalars.count + if count > sent { + var delta = String.UnicodeScalarView() + delta.append(contentsOf: scalars.dropFirst(sent)) + say(Event(event: "delta", text: String(delta))) + sent = count + } } + break + } catch LanguageModelSession.GenerationError.exceededContextWindowSize where sent == 0 && !turns.isEmpty { + let older = (turns.count + 1) / 2 + turns.removeFirst(older) + dropped += older + } catch let error as LanguageModelSession.GenerationError { + fail(code(error), error.localizedDescription) + } catch { + fail("generation_failed", String(describing: error)) } - } catch let error as LanguageModelSession.GenerationError { - fail(code(error), error.localizedDescription) - } catch { - fail("generation_failed", String(describing: error)) } var finish = Event(event: "finish") + if dropped > 0 { + finish.dropped = dropped + } if #available(macOS 26.4, *) { - let asked = entries + [.prompt(Transcript.Prompt(segments: text(request.prompt)))] - finish.inputTokens = try? await SystemLanguageModel.default.tokenCount(for: asked) - if let counted = try? await SystemLanguageModel.default.tokenCount(for: said), - let framing = try? await SystemLanguageModel.default.tokenCount(for: "") { + let model = SystemLanguageModel.default + finish.inputTokens = try? await model.tokenCount(for: head + turns.flatMap(exchange) + [asked]) + if let counted = try? await model.tokenCount(for: said), + let framing = try? await model.tokenCount(for: "") { finish.outputTokens = max(0, counted - framing) } + if let out = finish.outputTokens, let cap { + finish.reason = out >= cap ? "max_tokens" : "stop" + } } say(finish) } diff --git a/hives/apple-fm/src/bridge.rs b/hives/apple-fm/src/bridge.rs index df00d505..a6342952 100644 --- a/hives/apple-fm/src/bridge.rs +++ b/hives/apple-fm/src/bridge.rs @@ -7,7 +7,8 @@ //! becomes Apple's generation options: `temperature`, `season` as the most //! tokens it may grow, and `prune` and `seed` as how it samples, greedily //! at a temperature of 0. The bridge answers in NDJSON, each line a -//! [`Said`]. +//! [`Said`]; its finish says how many of the oldest turns it dropped to fit +//! the model's context. //! //! Apple's model grows words from words. A prompt that carries pollen or //! asks for a `trellis` is refused rather than grown as if it had not. @@ -55,6 +56,8 @@ pub enum Said { text: String, }, Finish { + reason: Option, + dropped: Option, #[serde(rename = "inputTokens")] input_tokens: Option, #[serde(rename = "outputTokens")] @@ -313,10 +316,13 @@ mod tests { let read = |s: &str| serde_json::from_str::(s).unwrap(); assert_eq!(read(r#"{"text":"hi","event":"delta"}"#), Said::Delta { text: "hi".into() }); assert_eq!( - read(r#"{"event":"finish","inputTokens":3,"outputTokens":2}"#), - Said::Finish { input_tokens: Some(3), output_tokens: Some(2) } + read(r#"{"event":"finish","reason":"max_tokens","dropped":2,"inputTokens":3,"outputTokens":2}"#), + Said::Finish { reason: Some("max_tokens".into()), dropped: Some(2), input_tokens: Some(3), output_tokens: Some(2) } + ); + assert_eq!( + read(r#"{"event":"finish"}"#), + Said::Finish { reason: None, dropped: None, input_tokens: None, output_tokens: None } ); - assert_eq!(read(r#"{"event":"finish"}"#), Said::Finish { input_tokens: None, output_tokens: None }); assert_eq!( read(r#"{"event":"error","code":"refusal","message":"no"}"#), Said::Error { code: "refusal".into(), message: "no".into() } diff --git a/hives/apple-fm/src/nurse.rs b/hives/apple-fm/src/nurse.rs index 4fdc855f..13c16b90 100644 --- a/hives/apple-fm/src/nurse.rs +++ b/hives/apple-fm/src/nurse.rs @@ -12,7 +12,9 @@ //! A cell lives for its sigil and remembers what it grew. A prompt that //! carries a graft is grown from that graft; one that carries none, as a //! forager that sends only the new words does, is grown from the turns the -//! cell has grown so far. So a sigil holds its conversation either way. +//! cell has grown so far. So a sigil holds its conversation either way. The +//! oldest turns the bridge dropped to fit the model's context, the cell +//! forgets too. //! //! A still kills the bridge mid-bloom and wilts the bloom `cancel`, and a //! prompt still waiting is told it was not grown. @@ -78,7 +80,8 @@ impl Nurse for AppleFm { Some(graft) if !graft.is_empty() => bridge::turns(graft), _ => grew.clone(), }; - if let Some(turn) = grow(&bridge, &prompt, &before, &tx_mmm, &stilled).await { + if let Some((turn, dropped)) = grow(&bridge, &prompt, &before, &tx_mmm, &stilled).await { + before.drain(..dropped.min(before.len())); before.push(turn); grew = before; } @@ -142,7 +145,7 @@ async fn grow( before: &[Turn], tx: &mpsc::Sender, stilled: &CancellationToken, -) -> Option { +) -> Option<(Turn, usize)> { let ask = match bridge::ask(prompt, before) { Ok(ask) => ask, Err(refusal) => { @@ -222,15 +225,23 @@ async fn grow( let _ = child.kill().await; unfurler.close().await; let _ = tx.send(json!({ "chi": "wilt", "reason": "stop" })).await; - return Some(turn(said)); + return Some((turn(said), 0)); } } - Said::Finish { input_tokens, output_tokens } => { + Said::Finish { reason, dropped, input_tokens, output_tokens } => { + let dropped = dropped.unwrap_or(0) as usize; + if dropped > 0 { + info!(dropped, kept = before.len().saturating_sub(dropped), "apple-fm.trimmed"); + } let thawed = frost.thaw(); said.push_str(&thawed); unfurler.grow(PetalKind::Text, &thawed).await; unfurler.close().await; - let ran_out = matches!((prompt.season, output_tokens), (Some(season), Some(out)) if out >= season); + let ran_out = match reason.as_deref() { + Some("max_tokens") => true, + Some("stop") => false, + _ => matches!((prompt.season, output_tokens), (Some(season), Some(out)) if out >= season), + }; let mut wilt = json!({ "chi": "wilt", "reason": if ran_out { "max_tokens" } else { "stop" } }); if input_tokens.is_some() || output_tokens.is_some() { wilt["nectar"] = json!({ @@ -239,13 +250,13 @@ async fn grow( }); } let _ = tx.send(wilt).await; - let _ = child.wait().await; - return Some(turn(said)); + let _ = child.kill().await; + return Some((turn(said), dropped)); } Said::Error { code, message } => { unfurler.close().await; let _ = tx.send(fail(&format!("apple_fm_{code}"), message)).await; - let _ = child.wait().await; + let _ = child.kill().await; return None; } Said::Probe { .. } => {} @@ -281,6 +292,9 @@ case "$words" in hang*) echo '{"event":"delta","text":"hanging"}'; exec sleep 30 ;; frost*) echo '{"event":"delta","text":"one two TH"}'; echo '{"event":"delta","text":"REE four"}'; exec sleep 30 ;; long*) echo '{"event":"delta","text":"on and on"}'; echo '{"event":"finish","inputTokens":4,"outputTokens":8}'; exit 0 ;; + capped*) echo '{"event":"delta","text":"cut"}'; echo '{"event":"finish","reason":"max_tokens","outputTokens":3}'; exit 0 ;; + trim*) echo '{"event":"delta","text":"trimmed"}'; echo '{"event":"finish","reason":"stop","dropped":2}'; exit 0 ;; + linger*) echo '{"event":"delta","text":"lingering"}'; echo '{"event":"finish"}'; exec sleep 30 ;; esac echo '{"event":"delta","text":"heard "}' printf '{"event":"delta","text":"%s. "}\n' "$words" @@ -444,6 +458,41 @@ echo '{"event":"finish","inputTokens":7,"outputTokens":3}' assert_eq!(ends(&closing(&cell, 1).await)[0]["reason"], "stop"); } + #[tokio::test] + async fn the_bridge_s_own_reason_is_the_wilt_s() { + let stub = Stub::new(); + let cell = raised(&stub).await; + cell.feed.send(fed("capped")).await.unwrap(); + assert_eq!(ends(&closing(&cell, 1).await)[0]["reason"], "max_tokens", "a reply the bridge held to the room left ran out"); + } + + #[tokio::test] + async fn the_turns_the_bridge_dropped_the_cell_forgets() { + let stub = Stub::new(); + let cell = raised(&stub).await; + for words in ["one", "two", "three", "trim now", "after"] { + cell.feed.send(fed(words)).await.unwrap(); + } + closing(&cell, 5).await; + let asked = stub.asked(); + assert_eq!(asked[3]["turns"].as_array().unwrap().len(), 3); + assert_eq!( + asked[4]["turns"], + json!([{"prompt": "three", "response": "heard three. "}, {"prompt": "trim now", "response": "trimmed"}]), + "the two oldest turns are gone" + ); + } + + #[tokio::test] + async fn a_bridge_that_lingers_after_its_finish_does_not_hold_the_cell() { + let stub = Stub::new(); + let cell = raised(&stub).await; + cell.feed.send(fed("linger")).await.unwrap(); + cell.feed.send(fed("next")).await.unwrap(); + let tones = closing(&cell, 2).await; + assert_eq!(said(&tones), "lingeringheard next. "); + } + #[tokio::test] async fn what_goes_wrong_is_told_and_the_cell_grows_on() { let stub = Stub::new(); diff --git a/hives/apple-fm/tests/live.rs b/hives/apple-fm/tests/live.rs index f344e924..abfc93ef 100644 --- a/hives/apple-fm/tests/live.rs +++ b/hives/apple-fm/tests/live.rs @@ -71,6 +71,21 @@ async fn a_conversation_grows_through_apple_s_model() { assert_eq!(seasoned.ended(), "wilt max_tokens", "{seasoned:?}"); assert_eq!(seasoned.end["nectar"]["output_tokens"], 12, "{seasoned:?}"); + let filler = json!({ + "content": [{"kind": "text", "text": "Tell me about the history of beekeeping in Europe. ".repeat(45)}], + "grew": [{"kind": "text", "text": "Beekeeping in Europe goes back thousands of years. ".repeat(45)}], + }); + let mut graft = vec![filler; 5]; + graft.push(json!({ + "content": [{"kind": "text", "text": "Name a red fruit in one word."}], + "grew": [{"kind": "text", "text": "Strawberry"}], + })); + f.send(ask("long", &m, "Which fruit did you just name? Reply with that one word.", json!({"temperature": 0.0, "graft": graft}))); + let long = f.bloom("long").await; + assert_eq!(long.ended(), "wilt stop", "a conversation past the model's context still grows: {long:?}"); + assert!(long.said.to_lowercase().contains("strawberry"), "the newest turns are the ones kept: {long:?}"); + nurse.wait_for("apple-fm.trimmed dropped=").await; + f.send(ask("cancel", &m, "Write a 2000 word essay about the history of beekeeping.", json!({}))); let first = f.on("cancel").await; assert_eq!(first["chi"], "unfurl", "{first}"); From 4de39aec384e58bdee43c3c933d2d85ced4ff246 Mon Sep 17 00:00:00 2001 From: Adil Date: Sun, 1 Nov 2026 18:53:35 +0500 Subject: [PATCH 4/4] hive/apple-fm: a bridge that falls silent is killed, and every reply fits its context A bridge that closed its stdout but lived on held its cell's next prompt forever while the nurse waited on it; the nurse now gives it two seconds and kills it, telling the bloom apple_fm_bridge_died. And a season larger than the room the conversation left is held to that room too, so no reply overflows the model's context mid-stream. --- hives/apple-fm/README.md | 10 +++++----- hives/apple-fm/bridge/gen.swift | 6 +++--- hives/apple-fm/src/nurse.rs | 26 +++++++++++++++++++++++--- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/hives/apple-fm/README.md b/hives/apple-fm/README.md index 516bc0b0..0ac412b7 100644 --- a/hives/apple-fm/README.md +++ b/hives/apple-fm/README.md @@ -75,9 +75,9 @@ A bloom grows through one run of the bridge: - **The reply.** Its words unfurl as a text petal as the model grows them, each unfurl only the words new since the last. The bloom wilts `stop`, or `max_tokens` when it grew all of its `season`, with Apple's - token counts as nectar. A reply with no `season` is held to the - context left after the conversation, so a long one wilts `max_tokens` - rather than overflowing. A `cancel` kills the bridge and wilts it + token counts as nectar. A reply is held to the context left after + the conversation, whatever its `season`, so a long one wilts + `max_tokens` rather than overflowing. A `cancel` kills the bridge and wilts it `cancel`. (Counts, and the room the reply is held to, need macOS 26.4 or later; before it the bridge drops the older half of the turns whenever Apple refuses a conversation as too long.) @@ -175,8 +175,8 @@ unnestled until the model is ready and then grows, one on a Mac that cannot run the model exits 0, and one whose bridge cannot run fails. Its cells' tests cover the turns a cell keeps, a graft that overrides them and the turns the bridge dropped, a still, a stop, a season that -ran out, a bridge that lingers after it finished, and each way a bridge -can fail; the unit tests cover the request a prompt becomes, its sampling, +ran out, a bridge that lingers after it finished or falls silent but +lives on, and each way a bridge can fail; the unit tests cover the request a prompt becomes, its sampling, what is refused, what the bridge says, and how `frost` holds back what may be a stop. diff --git a/hives/apple-fm/bridge/gen.swift b/hives/apple-fm/bridge/gen.swift index d9e7b5e8..b5d1aa4b 100644 --- a/hives/apple-fm/bridge/gen.swift +++ b/hives/apple-fm/bridge/gen.swift @@ -18,8 +18,8 @@ // The model's context is small (4096 tokens), so the oldest turns that do not // fit are dropped and counted in the finish's `dropped`: the newest turns are // kept that leave room for the reply, its `maximumResponseTokens` or 1024 -// tokens, and a reply with no `maximumResponseTokens` is held to the room that -// is left, so it ends `max_tokens` rather than overflowing. Where Apple cannot +// tokens, and the reply is held to the room that is left, so it ends +// `max_tokens` rather than overflowing. Where Apple cannot // count tokens, a transcript Apple refuses as too long is retried with the // older half of its turns dropped. // @@ -176,7 +176,7 @@ struct Gen { if #available(macOS 26.4, *), let fitted = await fit(turns, around: head + [asked], reserving: request.maximumResponseTokens) { dropped = turns.count - fitted.kept.count turns = fitted.kept - if cap == nil { cap = fitted.cap } + cap = min(cap ?? fitted.cap, fitted.cap) } let options = GenerationOptions( sampling: sampling(request.sampling), diff --git a/hives/apple-fm/src/nurse.rs b/hives/apple-fm/src/nurse.rs index 13c16b90..138ef5b7 100644 --- a/hives/apple-fm/src/nurse.rs +++ b/hives/apple-fm/src/nurse.rs @@ -23,6 +23,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::process::Stdio; use std::sync::Arc; +use std::time::Duration; use anyhow::Result; use async_trait::async_trait; @@ -263,9 +264,15 @@ async fn grow( } } - let status = child.wait().await.map(|s| s.to_string()).unwrap_or_else(|e| e.to_string()); + let status = match tokio::time::timeout(Duration::from_secs(2), child.wait()).await { + Ok(status) => status.map(|s| s.to_string()).unwrap_or_else(|e| e.to_string()), + Err(_) => { + let _ = child.kill().await; + "it closed its stdout and lived on, so it was killed".to_string() + } + }; let complaint = match complaint { - Some(task) => task.await.unwrap_or_default(), + Some(task) => tokio::time::timeout(Duration::from_secs(2), task).await.ok().and_then(Result::ok).unwrap_or_default(), None => String::new(), }; unfurler.close().await; @@ -278,7 +285,6 @@ async fn grow( #[cfg(test)] mod tests { use super::*; - use std::time::Duration; const STUB: &str = r#"#!/bin/sh here="$(cd "$(dirname "$0")" && pwd)" @@ -295,6 +301,7 @@ case "$words" in capped*) echo '{"event":"delta","text":"cut"}'; echo '{"event":"finish","reason":"max_tokens","outputTokens":3}'; exit 0 ;; trim*) echo '{"event":"delta","text":"trimmed"}'; echo '{"event":"finish","reason":"stop","dropped":2}'; exit 0 ;; linger*) echo '{"event":"delta","text":"lingering"}'; echo '{"event":"finish"}'; exec sleep 30 ;; + mute*) echo '{"event":"delta","text":"muted"}'; exec 1>&-; exec sleep 30 ;; esac echo '{"event":"delta","text":"heard "}' printf '{"event":"delta","text":"%s. "}\n' "$words" @@ -493,6 +500,19 @@ echo '{"event":"finish","inputTokens":7,"outputTokens":3}' assert_eq!(said(&tones), "lingeringheard next. "); } + #[tokio::test] + async fn a_bridge_that_falls_silent_but_lives_on_is_killed() { + let stub = Stub::new(); + let cell = raised(&stub).await; + cell.feed.send(fed("mute")).await.unwrap(); + cell.feed.send(fed("next")).await.unwrap(); + let tones = closing(&cell, 2).await; + let ends = ends(&tones); + assert_eq!(ends[0]["code"], "apple_fm_bridge_died"); + assert!(ends[0]["message"].as_str().unwrap().contains("killed"), "{}", ends[0]); + assert_eq!(ends[1]["reason"], "stop", "and the cell grows on"); + } + #[tokio::test] async fn what_goes_wrong_is_told_and_the_cell_grows_on() { let stub = Stub::new();