commit 26f81ff1ef67dbd15172de2838699dcfb489945d Author: Eric Wendland Date: Fri May 15 15:08:20 2026 +0200 Bootstrap geth Rust workspace diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bbf8fc2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1722 @@ +# 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", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[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 = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[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 0.2.17", + "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", +] + +[[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 = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[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 = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[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 = "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 = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[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 = "geth" +version = "0.1.0" +dependencies = [ + "anyhow", + "geth-cas", + "geth-cli", + "geth-config", + "geth-node", + "tempfile", + "tokio", + "tracing-subscriber", +] + +[[package]] +name = "geth-auth" +version = "0.1.0" +dependencies = [ + "geth-types", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "geth-cas" +version = "0.1.0" +dependencies = [ + "blake3", + "geth-types", + "hex", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "geth-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "geth-config", + "geth-control", + "geth-node", + "serde_json", + "tokio", +] + +[[package]] +name = "geth-codec" +version = "0.1.0" +dependencies = [ + "blake3", + "geth-types", + "hex", + "postcard", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "geth-config" +version = "0.1.0" +dependencies = [ + "directories", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "geth-control" +version = "0.1.0" +dependencies = [ + "geth-auth", + "geth-resource", + "geth-types", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "geth-crypto" +version = "0.1.0" +dependencies = [ + "blake3", + "ed25519-dalek", + "geth-types", + "hex", + "rand_core", + "serde", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "geth-db" +version = "0.1.0" +dependencies = [ + "geth-types", + "serde", +] + +[[package]] +name = "geth-discovery" +version = "0.1.0" +dependencies = [ + "geth-types", + "serde", +] + +[[package]] +name = "geth-document" +version = "0.1.0" +dependencies = [ + "geth-types", + "serde", +] + +[[package]] +name = "geth-iroh" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "geth-keychain" +version = "0.1.0" +dependencies = [ + "geth-types", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "geth-kv" +version = "0.1.0" +dependencies = [ + "geth-types", + "serde", +] + +[[package]] +name = "geth-node" +version = "0.1.0" +dependencies = [ + "geth-auth", + "geth-cas", + "geth-config", + "geth-control", + "geth-crypto", + "geth-resource", + "geth-store", + "geth-types", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "geth-pipe" +version = "0.1.0" +dependencies = [ + "geth-types", + "serde", +] + +[[package]] +name = "geth-pubsub" +version = "0.1.0" +dependencies = [ + "geth-types", + "serde", +] + +[[package]] +name = "geth-resource" +version = "0.1.0" +dependencies = [ + "geth-types", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "geth-secrets" +version = "0.1.0" +dependencies = [ + "geth-types", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "geth-ssh-identity" +version = "0.1.0" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "geth-ssh-proxy" +version = "0.1.0" +dependencies = [ + "geth-types", + "serde", +] + +[[package]] +name = "geth-store" +version = "0.1.0" +dependencies = [ + "geth-types", + "rusqlite", + "serde_json", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "geth-testkit" +version = "0.1.0" +dependencies = [ + "geth-config", + "geth-node", + "tempfile", +] + +[[package]] +name = "geth-types" +version = "0.1.0" +dependencies = [ + "serde", + "thiserror 2.0.18", +] + +[[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.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[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" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[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-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[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_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "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 = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[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 = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[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 = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[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 = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[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.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[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", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "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", +] + +[[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 = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "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.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bb1e048 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,60 @@ +[workspace] +members = [ + "crates/geth", + "crates/geth-cli", + "crates/geth-node", + "crates/geth-control", + "crates/geth-types", + "crates/geth-codec", + "crates/geth-config", + "crates/geth-store", + "crates/geth-crypto", + "crates/geth-ssh-identity", + "crates/geth-keychain", + "crates/geth-auth", + "crates/geth-secrets", + "crates/geth-resource", + "crates/geth-iroh", + "crates/geth-discovery", + "crates/geth-db", + "crates/geth-kv", + "crates/geth-pipe", + "crates/geth-document", + "crates/geth-pubsub", + "crates/geth-cas", + "crates/geth-ssh-proxy", + "crates/geth-testkit", +] +resolver = "3" + +[workspace.package] +edition = "2024" +license = "MIT OR Apache-2.0" +repository = "https://example.invalid/local/geth" +rust-version = "1.85" + +[workspace.dependencies] +anyhow = "1" +async-trait = "0.1" +base64 = "0.22" +blake3 = "1" +bytes = "1" +clap = { version = "4", features = ["derive", "env"] } +directories = "5" +ed25519-dalek = { version = "2", features = ["rand_core"] } +futures = "0.3" +hex = "0.4" +postcard = { version = "1", features = ["alloc"] } +rand_core = { version = "0.6", features = ["getrandom"] } +rusqlite = { version = "0.32", features = ["bundled"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tempfile = "3" +thiserror = "2" +time = { version = "0.3", features = ["formatting", "serde"] } +tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "rt-multi-thread", "signal", "time"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } + +[workspace.lints.rust] +unsafe_code = "forbid" diff --git a/README.md b/README.md new file mode 100644 index 0000000..cdc111e --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# geth + +`geth` is a personal, local-first mesh runtime for scripts, devices, databases, +documents, blobs, pipes, and future multi-user collaboration. + +This project is not the Ethereum `geth` client. The project and executable are +still named `geth`. + +## One Binary + +There is one executable: `geth`. + +It has daemon mode and control mode: + +```sh +geth init +geth daemon run +geth status +geth node id +geth resource list +geth cas add ./file +``` + +The daemon owns local identity, the future Iroh endpoint, trust state, resource +registry, module router, local metadata store, and synchronized data structures. +Most non-daemon commands talk to the daemon through a local Unix socket at +`$GETH_HOME/run/geth.sock`. + +## Transport And SSH + +All remote node-to-node geth communication is designed to happen over Iroh only. +SSH is not a geth transport backend, and there is no SSH fallback transport. + +SSH keys are used as admin trust anchors and ecosystem integration points. +OpenSSH, FIDO, and YubiKey-backed keys can sign geth trust objects through +explicit namespaces such as `geth.keychain.v1@geth.local`. Future SSH proxying +may carry SSH protocol bytes over authorized Iroh streams, but the geth transport +remains Iroh. + +## MVP Features + +The bootstrap implementation provides: + +- `geth init` +- `geth daemon run` +- `geth status` +- `geth node id` +- `geth resource list` +- `geth resource create ` +- `geth keychain status` +- `geth auth explain ` +- local filesystem CAS commands: `add`, `get`, `hash`, `has`, `list` + +Other command groups exist as explicit stubs: `db`, `kv`, `pipe`, `document`, +`pubsub`, `secret`, and `ssh`. + +## Resource Modules + +Everything meaningful is modeled as a resource. Planned resource kinds are: + +- `db`: SQLite/cr-sqlite synchronization +- `kv`: Iroh Documents backed key-value stores +- `pipe`: dumbpipe-like byte streams over Iroh +- `document`: Automerge documents over Iroh streams +- `pubsub`: lossy notifications, not authoritative storage +- `cas`: content-addressed blob storage and distribution +- `ssh-proxy`: authorized SSH proxy/admin access over Iroh + +Authorization is resource-scoped and capability-based. Bearer secrets may grant +specific resource capabilities but do not create trusted node identity. + +## Local State + +If `GETH_HOME` is set, geth uses it. Otherwise it uses an OS-specific data +directory. The bootstrap layout is: + +```text +$GETH_HOME/ + geth.sqlite + config.toml + identity/agent.ed25519 + cas/blobs/ + run/geth.sock +``` + +## Quick Start + +In one shell: + +```sh +export GETH_HOME="$(mktemp -d)" +cargo run -p geth -- init +cargo run -p geth -- daemon run +``` + +In another shell: + +```sh +export GETH_HOME="" +cargo run -p geth -- status +cargo run -p geth -- node id +echo "hello geth" > /tmp/hello-geth.txt +cargo run -p geth -- cas add /tmp/hello-geth.txt +cargo run -p geth -- cas list +``` + +## Authorization Direction + +The MVP defines the split between: + +- keychain: SSH-rooted users, devices, nodes, agents, and endpoint bindings +- auth: resource-local signed authorization operations and capability grants +- secrets: resource master secrets, epochs, envelopes, and bearer access + +The current code does not implement Keyhive, BeeKEM, strong forward secrecy, or +post-compromise security. It leaves room for future local-first, replicated auth +logs and BeeKEM/CGKA-style group key evolution. diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..cda8d17 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +avoid-breaking-exported-api = false diff --git a/crates/geth-auth/Cargo.toml b/crates/geth-auth/Cargo.toml new file mode 100644 index 0000000..cd6fd3b --- /dev/null +++ b/crates/geth-auth/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "geth-auth" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +thiserror.workspace = true +geth-types = { path = "../geth-types" } + +[dev-dependencies] +serde_json.workspace = true diff --git a/crates/geth-auth/src/lib.rs b/crates/geth-auth/src/lib.rs new file mode 100644 index 0000000..a52ef71 --- /dev/null +++ b/crates/geth-auth/src/lib.rs @@ -0,0 +1,96 @@ +use geth_types::{AuthOpId, Capability, GroupId, PrincipalId, ResourceId, SecretId, UnixMillis}; +use serde::{Deserialize, Serialize}; + +pub const AUTH_SIGNATURE_NAMESPACE: &str = "geth.auth-op.v1@geth.local"; +pub const RESOURCE_GRANT_SIGNATURE_NAMESPACE: &str = "geth.resource-grant.v1@geth.local"; +pub const REVOCATION_SIGNATURE_NAMESPACE: &str = "geth.revocation.v1@geth.local"; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct AuthOp { + pub id: AuthOpId, + pub resource: ResourceId, + pub created_at: UnixMillis, + pub kind: AuthOpKind, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "kebab-case")] +pub enum AuthOpKind { + ResourceCreate, + ResourceAuthoritySet { + authority: ResourceId, + }, + GrantCreate { + grant_id: String, + principal: PrincipalId, + capabilities: Vec, + }, + GrantRevoke { + grant_id: String, + }, + BearerAccessCreate { + secret: SecretId, + capabilities: Vec, + expires_at: Option, + }, + BearerAccessRevoke { + secret: SecretId, + }, + GroupCreate { + group: GroupId, + }, + GroupAddMember { + group: GroupId, + principal: PrincipalId, + }, + GroupRemoveMember { + group: GroupId, + principal: PrincipalId, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct AuthExplanation { + pub subject: String, + pub resource: String, + pub capability: String, + pub allowed: bool, + pub reason: String, + pub evaluated_ops: usize, +} + +impl AuthExplanation { + #[must_use] + pub fn stub(subject: String, resource: String, capability: String) -> Self { + Self { + subject, + resource, + capability, + allowed: false, + reason: "authorization logs are scaffolded; no grant reducer is active yet".to_owned(), + evaluated_ops: 0, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn auth_structs_roundtrip() { + let op = AuthOp { + id: "op:auth:1".into(), + resource: "resource:notes".into(), + created_at: UnixMillis(10), + kind: AuthOpKind::GrantCreate { + grant_id: "grant:1".to_owned(), + principal: "node:laptop".into(), + capabilities: vec!["kv.read".into(), "kv.write_prefix:apps/foo/".into()], + }, + }; + let json = serde_json::to_string(&op).expect("json"); + let decoded: AuthOp = serde_json::from_str(&json).expect("decode"); + assert_eq!(decoded, op); + } +} diff --git a/crates/geth-cas/Cargo.toml b/crates/geth-cas/Cargo.toml new file mode 100644 index 0000000..d3b8df2 --- /dev/null +++ b/crates/geth-cas/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "geth-cas" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +blake3.workspace = true +hex.workspace = true +thiserror.workspace = true +geth-types = { path = "../geth-types" } + +[dev-dependencies] +tempfile.workspace = true diff --git a/crates/geth-cas/src/lib.rs b/crates/geth-cas/src/lib.rs new file mode 100644 index 0000000..ceee527 --- /dev/null +++ b/crates/geth-cas/src/lib.rs @@ -0,0 +1,157 @@ +use geth_types::BlobHash; +use std::path::{Path, PathBuf}; + +#[derive(Debug, thiserror::Error)] +pub enum CasError { + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("invalid blob hash: {0}")] + InvalidHash(String), + #[error("blob not found: {0}")] + NotFound(String), +} + +#[derive(Clone, Debug)] +pub struct LocalCas { + root: PathBuf, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BlobInfo { + pub hash: BlobHash, + pub size_bytes: u64, + pub path: PathBuf, +} + +impl LocalCas { + #[must_use] + pub fn new(root: impl Into) -> Self { + Self { root: root.into() } + } + + pub fn add_path(&self, path: &Path) -> Result { + let bytes = std::fs::read(path)?; + self.add_bytes(&bytes) + } + + pub fn add_bytes(&self, bytes: &[u8]) -> Result { + let hash = hash_bytes(bytes); + let path = self.blob_path(&hash)?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + if !path.exists() { + let tmp = path.with_extension("tmp"); + std::fs::write(&tmp, bytes)?; + std::fs::rename(tmp, &path)?; + } + Ok(BlobInfo { + hash, + size_bytes: bytes.len() as u64, + path, + }) + } + + pub fn get_to_path(&self, hash: &BlobHash, out: &Path) -> Result { + let path = self.blob_path(hash)?; + if !path.exists() { + return Err(CasError::NotFound(hash.to_string())); + } + if let Some(parent) = out.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::copy(path, out).map_err(CasError::from) + } + + pub fn has(&self, hash: &BlobHash) -> Result { + Ok(self.blob_path(hash)?.exists()) + } + + pub fn list(&self) -> Result, CasError> { + let blobs = self.root.join("blobs"); + if !blobs.exists() { + return Ok(Vec::new()); + } + let mut infos = Vec::new(); + for first in std::fs::read_dir(blobs)? { + let first = first?; + if !first.file_type()?.is_dir() { + continue; + } + for second in std::fs::read_dir(first.path())? { + let second = second?; + if !second.file_type()?.is_dir() { + continue; + } + for entry in std::fs::read_dir(second.path())? { + let entry = entry?; + if !entry.file_type()?.is_file() { + continue; + } + let hash = entry.file_name().to_string_lossy().to_string(); + if is_valid_hash(&hash) { + let meta = entry.metadata()?; + infos.push(BlobInfo { + hash: BlobHash::new(hash), + size_bytes: meta.len(), + path: entry.path(), + }); + } + } + } + } + infos.sort_by(|a, b| a.hash.as_str().cmp(b.hash.as_str())); + Ok(infos) + } + + pub fn blob_path(&self, hash: &BlobHash) -> Result { + validate_hash(hash)?; + let hash = hash.as_str(); + Ok(self + .root + .join("blobs") + .join(&hash[0..2]) + .join(&hash[2..4]) + .join(hash)) + } +} + +#[must_use] +pub fn hash_bytes(bytes: &[u8]) -> BlobHash { + BlobHash::new(blake3::hash(bytes).to_hex().to_string()) +} + +pub fn hash_path(path: &Path) -> Result { + let bytes = std::fs::read(path)?; + Ok(hash_bytes(&bytes)) +} + +pub fn validate_hash(hash: &BlobHash) -> Result<(), CasError> { + if is_valid_hash(hash.as_str()) { + Ok(()) + } else { + Err(CasError::InvalidHash(hash.to_string())) + } +} + +#[must_use] +pub fn is_valid_hash(hash: &str) -> bool { + hash.len() == 64 && hash.bytes().all(|byte| byte.is_ascii_hexdigit()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cas_add_get_has_list() { + let dir = tempfile::tempdir().expect("tempdir"); + let cas = LocalCas::new(dir.path()); + let info = cas.add_bytes(b"hello geth").expect("add"); + assert!(cas.has(&info.hash).expect("has")); + assert_eq!(cas.list().expect("list").len(), 1); + let out = dir.path().join("out.txt"); + cas.get_to_path(&info.hash, &out).expect("get"); + assert_eq!(std::fs::read(out).expect("read"), b"hello geth"); + } +} diff --git a/crates/geth-cli/Cargo.toml b/crates/geth-cli/Cargo.toml new file mode 100644 index 0000000..1fadfe9 --- /dev/null +++ b/crates/geth-cli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "geth-cli" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +clap.workspace = true +serde_json.workspace = true +tokio.workspace = true +geth-config = { path = "../geth-config" } +geth-control = { path = "../geth-control" } +geth-node = { path = "../geth-node" } diff --git a/crates/geth-cli/src/lib.rs b/crates/geth-cli/src/lib.rs new file mode 100644 index 0000000..9658703 --- /dev/null +++ b/crates/geth-cli/src/lib.rs @@ -0,0 +1,365 @@ +use anyhow::{Context, Result, bail}; +use clap::{Args, Parser, Subcommand}; +use geth_config::GethPaths; +use geth_control::{ControlRequest, ControlResponse}; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +#[command(name = "geth", about = "Personal local-first Iroh mesh runtime")] +pub struct Cli { + #[arg(long, global = true)] + pub json: bool, + #[arg(long, global = true)] + pub jsonl: bool, + #[command(subcommand)] + pub command: Command, +} + +#[derive(Debug, Subcommand)] +pub enum Command { + Init, + Daemon { + #[command(subcommand)] + command: DaemonCommand, + }, + Status, + Node { + #[command(subcommand)] + command: NodeCommand, + }, + Resource { + #[command(subcommand)] + command: ResourceCommand, + }, + Keychain { + #[command(subcommand)] + command: KeychainCommand, + }, + Auth { + #[command(subcommand)] + command: AuthCommand, + }, + Secret { + #[command(subcommand)] + command: SecretCommand, + }, + Cas { + #[command(subcommand)] + command: CasCommand, + }, + Kv { + #[command(subcommand)] + command: KvCommand, + }, + Pubsub { + #[command(subcommand)] + command: PubsubCommand, + }, + Pipe { + #[command(subcommand)] + command: PipeCommand, + }, + Db { + #[command(subcommand)] + command: DbCommand, + }, + Document { + #[command(subcommand)] + command: DocumentCommand, + }, + Ssh { + #[command(subcommand)] + command: SshCommand, + }, +} + +#[derive(Debug, Subcommand)] +pub enum DaemonCommand { + Run, +} + +#[derive(Debug, Subcommand)] +pub enum NodeCommand { + Id, + Status, +} + +#[derive(Debug, Subcommand)] +pub enum ResourceCommand { + List, + Create { kind: String, name: String }, +} + +#[derive(Debug, Subcommand)] +pub enum KeychainCommand { + Init, + Status, +} + +#[derive(Debug, Subcommand)] +pub enum AuthCommand { + Explain { + subject: String, + resource: String, + capability: String, + }, +} + +#[derive(Debug, Subcommand)] +pub enum SecretCommand { + Status, +} + +#[derive(Debug, Subcommand)] +pub enum CasCommand { + Add { + path: PathBuf, + }, + Get { + hash: String, + #[arg(long)] + out: PathBuf, + }, + Hash { + path: PathBuf, + }, + Has { + hash: String, + }, + List, +} + +#[derive(Debug, Subcommand)] +pub enum KvCommand { + Create { + name: String, + }, + Set { + name: String, + key: String, + value: String, + }, + Get { + name: String, + key: String, + }, +} + +#[derive(Debug, Subcommand)] +pub enum PubsubCommand { + Pub { topic: String, message: String }, + Sub { topic: String }, +} + +#[derive(Debug, Subcommand)] +pub enum PipeCommand { + Listen { name: String }, + Connect { target: String }, +} + +#[derive(Debug, Subcommand)] +pub enum DbCommand { + Add { name: String, path: PathBuf }, + Status { name: String }, +} + +#[derive(Debug, Subcommand)] +pub enum DocumentCommand { + Create { name: String }, + Status { name: String }, +} + +#[derive(Debug, Subcommand)] +pub enum SshCommand { + Proxy { node: String }, +} + +#[derive(Debug, Args)] +pub struct EmptyArgs {} + +pub async fn run() -> Result<()> { + let cli = Cli::parse(); + let paths = GethPaths::resolve().context("resolve geth paths")?; + match cli.command { + Command::Init => { + let node = geth_node::init_node(&paths).context("initialize geth node")?; + println!("initialized geth home: {}", node.paths.home().display()); + println!("agent: {}", node.agent_id); + println!("node: {}", node.node_id); + } + Command::Daemon { + command: DaemonCommand::Run, + } => { + geth_node::run_daemon(paths) + .await + .context("run geth daemon")?; + } + command => { + let request = request_for_command(command)?; + let response = geth_node::send_control(&paths, request) + .await + .with_context(|| { + format!("connect to daemon at {}", paths.socket_path().display()) + })?; + print_response(response, cli.json || cli.jsonl)?; + } + } + Ok(()) +} + +fn request_for_command(command: Command) -> Result { + Ok(match command { + Command::Status => ControlRequest::Status, + Command::Node { + command: NodeCommand::Id, + } => ControlRequest::NodeId, + Command::Node { + command: NodeCommand::Status, + } => ControlRequest::Status, + Command::Resource { + command: ResourceCommand::List, + } => ControlRequest::ResourceList, + Command::Resource { + command: ResourceCommand::Create { kind, name }, + } => ControlRequest::ResourceCreate { kind, name }, + Command::Keychain { + command: KeychainCommand::Init, + } => ControlRequest::ModuleStub { + module: "keychain".to_owned(), + command: "init".to_owned(), + }, + Command::Keychain { + command: KeychainCommand::Status, + } => ControlRequest::KeychainStatus, + Command::Auth { + command: + AuthCommand::Explain { + subject, + resource, + capability, + }, + } => ControlRequest::AuthExplain { + subject, + resource, + capability, + }, + Command::Secret { command } => ControlRequest::ModuleStub { + module: "secret".to_owned(), + command: format!("{command:?}"), + }, + Command::Cas { command } => match command { + CasCommand::Add { path } => ControlRequest::CasAdd { path }, + CasCommand::Get { hash, out } => ControlRequest::CasGet { + hash: hash.into(), + out, + }, + CasCommand::Hash { path } => ControlRequest::CasHash { path }, + CasCommand::Has { hash } => ControlRequest::CasHas { hash: hash.into() }, + CasCommand::List => ControlRequest::CasList, + }, + Command::Kv { command } => ControlRequest::ModuleStub { + module: "kv".to_owned(), + command: format!("{command:?}"), + }, + Command::Pubsub { command } => ControlRequest::ModuleStub { + module: "pubsub".to_owned(), + command: format!("{command:?}"), + }, + Command::Pipe { command } => ControlRequest::ModuleStub { + module: "pipe".to_owned(), + command: format!("{command:?}"), + }, + Command::Db { command } => ControlRequest::ModuleStub { + module: "db".to_owned(), + command: format!("{command:?}"), + }, + Command::Document { command } => ControlRequest::ModuleStub { + module: "document".to_owned(), + command: format!("{command:?}"), + }, + Command::Ssh { command } => ControlRequest::ModuleStub { + module: "ssh-proxy".to_owned(), + command: format!("{command:?}"), + }, + Command::Init | Command::Daemon { .. } => bail!("command is handled directly"), + }) +} + +fn print_response(response: ControlResponse, json: bool) -> Result<()> { + if json { + println!("{}", serde_json::to_string_pretty(&response)?); + return Ok(()); + } + match response { + ControlResponse::Status(status) => { + println!("geth daemon: running"); + println!("home: {}", status.home.display()); + println!("socket: {}", status.socket.display()); + println!("agent: {}", status.agent_id); + println!("node: {}", status.node_id); + println!("iroh: {}", status.iroh); + } + ControlResponse::NodeId(node) => { + println!("agent: {}", node.agent_id); + println!("node: {}", node.node_id); + println!( + "endpoint: {}", + node.endpoint_id + .as_deref() + .unwrap_or("not started in bootstrap") + ); + } + ControlResponse::ResourceList { resources } => { + if resources.is_empty() { + println!("no resources"); + } else { + for resource in resources { + println!("{}\t{}\t{}", resource.kind, resource.name, resource.id); + } + } + } + ControlResponse::ResourceCreated { resource } => { + println!( + "created resource: {} {} ({})", + resource.kind, resource.name, resource.id + ); + } + ControlResponse::CasAdded { hash, size_bytes } => { + println!("{hash} {size_bytes} bytes"); + } + ControlResponse::CasGot { + hash, + out, + size_bytes, + } => { + println!("wrote {hash} to {} ({size_bytes} bytes)", out.display()); + } + ControlResponse::CasHash { hash } => println!("{hash}"), + ControlResponse::CasHas { hash, present } => println!("{hash}: {present}"), + ControlResponse::CasList { blobs } => { + for blob in blobs { + println!("{}\t{} bytes", blob.hash, blob.size_bytes); + } + } + ControlResponse::KeychainStatus(status) => { + println!("initialized: {}", status.initialized); + println!("admin_keys: {}", status.admin_keys); + println!("users: {}", status.users); + println!("devices: {}", status.devices); + println!("nodes: {}", status.nodes); + } + ControlResponse::AuthExplain(explain) => { + println!("allowed: {}", explain.allowed); + println!("subject: {}", explain.subject); + println!("resource: {}", explain.resource); + println!("capability: {}", explain.capability); + println!("reason: {}", explain.reason); + println!("evaluated_ops: {}", explain.evaluated_ops); + } + ControlResponse::NotImplemented { module, command } => { + println!("{module} {command}: not implemented yet"); + } + ControlResponse::Error { message } => bail!(message), + } + Ok(()) +} diff --git a/crates/geth-codec/Cargo.toml b/crates/geth-codec/Cargo.toml new file mode 100644 index 0000000..914b86e --- /dev/null +++ b/crates/geth-codec/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "geth-codec" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +blake3.workspace = true +hex.workspace = true +postcard.workspace = true +serde.workspace = true +thiserror.workspace = true +geth-types = { path = "../geth-types" } diff --git a/crates/geth-codec/src/lib.rs b/crates/geth-codec/src/lib.rs new file mode 100644 index 0000000..fe3c565 --- /dev/null +++ b/crates/geth-codec/src/lib.rs @@ -0,0 +1,62 @@ +use serde::{Serialize, de::DeserializeOwned}; + +#[derive(Debug, thiserror::Error)] +pub enum CodecError { + #[error("canonical encoding failed: {0}")] + Encode(#[from] postcard::Error), +} + +pub fn encode_canonical(value: &T) -> Result, CodecError> { + postcard::to_allocvec(value).map_err(CodecError::from) +} + +pub fn decode_canonical(bytes: &[u8]) -> Result { + postcard::from_bytes(bytes).map_err(CodecError::from) +} + +pub fn hash_canonical( + value: &T, +) -> Result { + let bytes = encode_canonical(value)?; + Ok(blake3_hash_bytes(&bytes)) +} + +#[must_use] +pub fn blake3_hash_bytes(bytes: &[u8]) -> geth_types::BlobHash { + geth_types::BlobHash::new(blake3::hash(bytes).to_hex().to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct Sample { + version: u8, + name: String, + values: Vec, + } + + #[test] + fn canonical_encoding_is_deterministic() { + let sample = Sample { + version: 1, + name: "geth".to_owned(), + values: vec![1, 2, 3], + }; + assert_eq!( + encode_canonical(&sample).expect("encode"), + encode_canonical(&sample).expect("encode again") + ); + assert_eq!( + hash_canonical(&sample).expect("hash"), + hash_canonical(&sample).expect("hash again") + ); + assert_eq!( + decode_canonical::(&encode_canonical(&sample).expect("encode")) + .expect("decode"), + sample + ); + } +} diff --git a/crates/geth-config/Cargo.toml b/crates/geth-config/Cargo.toml new file mode 100644 index 0000000..f2ed90b --- /dev/null +++ b/crates/geth-config/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "geth-config" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +directories.workspace = true +serde.workspace = true +thiserror.workspace = true diff --git a/crates/geth-config/src/lib.rs b/crates/geth-config/src/lib.rs new file mode 100644 index 0000000..e4e12e3 --- /dev/null +++ b/crates/geth-config/src/lib.rs @@ -0,0 +1,81 @@ +use std::path::{Path, PathBuf}; + +#[derive(Clone, Debug)] +pub struct GethPaths { + home: PathBuf, +} + +impl GethPaths { + pub fn resolve() -> Result { + if let Some(home) = std::env::var_os("GETH_HOME") { + return Ok(Self { + home: PathBuf::from(home), + }); + } + let project_dirs = directories::ProjectDirs::from("local", "geth", "geth") + .ok_or(ConfigError::NoDataDirectory)?; + Ok(Self { + home: project_dirs.data_dir().to_path_buf(), + }) + } + + #[must_use] + pub fn from_home(home: impl Into) -> Self { + Self { home: home.into() } + } + + #[must_use] + pub fn home(&self) -> &Path { + &self.home + } + + #[must_use] + pub fn config_file(&self) -> PathBuf { + self.home.join("config.toml") + } + + #[must_use] + pub fn metadata_db(&self) -> PathBuf { + self.home.join("geth.sqlite") + } + + #[must_use] + pub fn identity_dir(&self) -> PathBuf { + self.home.join("identity") + } + + #[must_use] + pub fn agent_key(&self) -> PathBuf { + self.identity_dir().join("agent.ed25519") + } + + #[must_use] + pub fn cas_dir(&self) -> PathBuf { + self.home.join("cas") + } + + #[must_use] + pub fn run_dir(&self) -> PathBuf { + self.home.join("run") + } + + #[must_use] + pub fn socket_path(&self) -> PathBuf { + self.run_dir().join("geth.sock") + } + + pub fn ensure_base_dirs(&self) -> Result<(), ConfigError> { + std::fs::create_dir_all(self.identity_dir())?; + std::fs::create_dir_all(self.cas_dir().join("blobs"))?; + std::fs::create_dir_all(self.run_dir())?; + Ok(()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ConfigError { + #[error("could not determine OS data directory and GETH_HOME is unset")] + NoDataDirectory, + #[error("io error: {0}")] + Io(#[from] std::io::Error), +} diff --git a/crates/geth-control/Cargo.toml b/crates/geth-control/Cargo.toml new file mode 100644 index 0000000..abb89f7 --- /dev/null +++ b/crates/geth-control/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "geth-control" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +geth-auth = { path = "../geth-auth" } +geth-resource = { path = "../geth-resource" } +geth-types = { path = "../geth-types" } diff --git a/crates/geth-control/src/lib.rs b/crates/geth-control/src/lib.rs new file mode 100644 index 0000000..a7a09ae --- /dev/null +++ b/crates/geth-control/src/lib.rs @@ -0,0 +1,164 @@ +use geth_auth::AuthExplanation; +use geth_resource::ResourceDescriptor; +use geth_types::BlobHash; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +pub enum ControlRequest { + Status, + NodeId, + ResourceList, + ResourceCreate { + kind: String, + name: String, + }, + CasAdd { + path: PathBuf, + }, + CasGet { + hash: BlobHash, + out: PathBuf, + }, + CasHash { + path: PathBuf, + }, + CasHas { + hash: BlobHash, + }, + CasList, + KeychainStatus, + AuthExplain { + subject: String, + resource: String, + capability: String, + }, + ModuleStub { + module: String, + command: String, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +pub enum ControlResponse { + Status(StatusResponse), + NodeId(NodeIdResponse), + ResourceList { + resources: Vec, + }, + ResourceCreated { + resource: ResourceDescriptor, + }, + CasAdded { + hash: BlobHash, + size_bytes: u64, + }, + CasGot { + hash: BlobHash, + out: PathBuf, + size_bytes: u64, + }, + CasHash { + hash: BlobHash, + }, + CasHas { + hash: BlobHash, + present: bool, + }, + CasList { + blobs: Vec, + }, + KeychainStatus(KeychainStatusResponse), + AuthExplain(AuthExplanation), + NotImplemented { + module: String, + command: String, + }, + Error { + message: String, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct StatusResponse { + pub home: PathBuf, + pub socket: PathBuf, + pub agent_id: String, + pub node_id: String, + pub iroh: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct NodeIdResponse { + pub agent_id: String, + pub node_id: String, + pub endpoint_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct KeychainStatusResponse { + pub initialized: bool, + pub admin_keys: usize, + pub users: usize, + pub devices: usize, + pub nodes: usize, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CasBlob { + pub hash: BlobHash, + pub size_bytes: u64, +} + +#[derive(Debug, thiserror::Error)] +pub enum ControlError { + #[error("json error: {0}")] + Json(#[from] serde_json::Error), +} + +pub fn encode_request(request: &ControlRequest) -> Result { + let mut line = serde_json::to_string(request)?; + line.push('\n'); + Ok(line) +} + +pub fn decode_request(line: &str) -> Result { + serde_json::from_str(line).map_err(ControlError::from) +} + +pub fn encode_response(response: &ControlResponse) -> Result { + let mut line = serde_json::to_string(response)?; + line.push('\n'); + Ok(line) +} + +pub fn decode_response(line: &str) -> Result { + serde_json::from_str(line).map_err(ControlError::from) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn control_request_response_serialization_roundtrip() { + let request = ControlRequest::CasHas { + hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(), + }; + assert_eq!( + decode_request(&encode_request(&request).expect("encode")).expect("decode"), + request + ); + + let response = ControlResponse::CasHas { + hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(), + present: true, + }; + assert_eq!( + decode_response(&encode_response(&response).expect("encode")).expect("decode"), + response + ); + } +} diff --git a/crates/geth-crypto/Cargo.toml b/crates/geth-crypto/Cargo.toml new file mode 100644 index 0000000..4fdcd06 --- /dev/null +++ b/crates/geth-crypto/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "geth-crypto" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +blake3.workspace = true +ed25519-dalek.workspace = true +hex.workspace = true +rand_core.workspace = true +serde.workspace = true +thiserror.workspace = true +geth-types = { path = "../geth-types" } + +[dev-dependencies] +tempfile.workspace = true diff --git a/crates/geth-crypto/src/lib.rs b/crates/geth-crypto/src/lib.rs new file mode 100644 index 0000000..59e3e75 --- /dev/null +++ b/crates/geth-crypto/src/lib.rs @@ -0,0 +1,118 @@ +use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; +use rand_core::OsRng; +use std::path::Path; + +#[derive(Debug, thiserror::Error)] +pub enum CryptoError { + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("invalid hex key: {0}")] + Hex(#[from] hex::FromHexError), + #[error("invalid ed25519 key material")] + InvalidKey, + #[error("signature verification failed")] + Verify, +} + +pub struct AgentKey { + signing_key: SigningKey, +} + +impl AgentKey { + #[must_use] + pub fn generate() -> Self { + Self { + signing_key: SigningKey::generate(&mut OsRng), + } + } + + pub fn load_or_create(path: &Path) -> Result { + if path.exists() { + return Self::load(path); + } + let key = Self::generate(); + key.save(path)?; + Ok(key) + } + + pub fn load(path: &Path) -> Result { + let hex_key = std::fs::read_to_string(path)?; + let bytes = hex::decode(hex_key.trim())?; + let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| CryptoError::InvalidKey)?; + Ok(Self { + signing_key: SigningKey::from_bytes(&key_bytes), + }) + } + + pub fn save(&self, path: &Path) -> Result<(), CryptoError> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let tmp = path.with_extension("tmp"); + std::fs::write(&tmp, hex::encode(self.signing_key.to_bytes()))?; + std::fs::rename(tmp, path)?; + Ok(()) + } + + #[must_use] + pub fn verifying_key(&self) -> VerifyingKey { + self.signing_key.verifying_key() + } + + #[must_use] + pub fn public_key_hex(&self) -> String { + hex::encode(self.verifying_key().to_bytes()) + } + + #[must_use] + pub fn agent_id(&self) -> geth_types::AgentId { + geth_types::AgentId::new(key_fingerprint(&self.verifying_key().to_bytes())) + } + + #[must_use] + pub fn sign(&self, bytes: &[u8]) -> Vec { + self.signing_key.sign(bytes).to_bytes().to_vec() + } +} + +pub fn verify(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), CryptoError> { + let key_bytes: [u8; 32] = public_key.try_into().map_err(|_| CryptoError::InvalidKey)?; + let verifying_key = + VerifyingKey::from_bytes(&key_bytes).map_err(|_| CryptoError::InvalidKey)?; + let sig = Signature::from_slice(signature).map_err(|_| CryptoError::InvalidKey)?; + verifying_key + .verify(message, &sig) + .map_err(|_| CryptoError::Verify) +} + +#[must_use] +pub fn blake3_hex(bytes: &[u8]) -> String { + blake3::hash(bytes).to_hex().to_string() +} + +#[must_use] +pub fn key_fingerprint(public_key: &[u8]) -> String { + format!("ed25519:{}", blake3_hex(public_key)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn agent_identity_persists() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("agent.ed25519"); + let first = AgentKey::load_or_create(&path).expect("create"); + let second = AgentKey::load_or_create(&path).expect("load"); + assert_eq!(first.agent_id(), second.agent_id()); + } + + #[test] + fn blake3_helper_matches_known_hash() { + assert_eq!( + blake3_hex(b"hello geth"), + "3a4aa805ade0d4694a1bb69ad5b9a2f1dffcd4de2136df9a465023722d26e325" + ); + } +} diff --git a/crates/geth-db/Cargo.toml b/crates/geth-db/Cargo.toml new file mode 100644 index 0000000..2fab618 --- /dev/null +++ b/crates/geth-db/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "geth-db" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +geth-types = { path = "../geth-types" } diff --git a/crates/geth-db/src/lib.rs b/crates/geth-db/src/lib.rs new file mode 100644 index 0000000..d120ffe --- /dev/null +++ b/crates/geth-db/src/lib.rs @@ -0,0 +1,15 @@ +use geth_types::{DbId, ResourceId}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct DbResource { + pub id: DbId, + pub resource: ResourceId, + pub path: String, + pub sync_status: String, +} + +#[must_use] +pub fn crsqlite_sync_roadmap() -> &'static str { + "future db sync reads crsql_changes, exchanges changes over Iroh, and applies through crsql_changes" +} diff --git a/crates/geth-discovery/Cargo.toml b/crates/geth-discovery/Cargo.toml new file mode 100644 index 0000000..991b5ec --- /dev/null +++ b/crates/geth-discovery/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "geth-discovery" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +geth-types = { path = "../geth-types" } diff --git a/crates/geth-discovery/src/lib.rs b/crates/geth-discovery/src/lib.rs new file mode 100644 index 0000000..70aa49e --- /dev/null +++ b/crates/geth-discovery/src/lib.rs @@ -0,0 +1,18 @@ +use geth_types::NodeId; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PeerCard { + pub node: NodeId, + pub endpoints: Vec, + pub signed_by: String, +} + +pub trait DiscoveryBackend { + fn candidates(&self) -> Vec; +} + +#[must_use] +pub fn discovery_is_untrusted_note() -> &'static str { + "discovery returns candidate peers only and never grants trust or authorization" +} diff --git a/crates/geth-document/Cargo.toml b/crates/geth-document/Cargo.toml new file mode 100644 index 0000000..bb16f82 --- /dev/null +++ b/crates/geth-document/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "geth-document" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +geth-types = { path = "../geth-types" } diff --git a/crates/geth-document/src/lib.rs b/crates/geth-document/src/lib.rs new file mode 100644 index 0000000..672cfdb --- /dev/null +++ b/crates/geth-document/src/lib.rs @@ -0,0 +1,14 @@ +use geth_types::{DocumentId, ResourceId}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct DocumentResource { + pub id: DocumentId, + pub resource: ResourceId, + pub name: String, +} + +#[must_use] +pub fn automerge_roadmap() -> &'static str { + "future documents use Automerge sync over Iroh with resource-local authorization" +} diff --git a/crates/geth-iroh/Cargo.toml b/crates/geth-iroh/Cargo.toml new file mode 100644 index 0000000..9a78bfe --- /dev/null +++ b/crates/geth-iroh/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "geth-iroh" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true diff --git a/crates/geth-iroh/src/lib.rs b/crates/geth-iroh/src/lib.rs new file mode 100644 index 0000000..f13724d --- /dev/null +++ b/crates/geth-iroh/src/lib.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; + +pub const ALPN_CONTROL: &[u8] = b"/geth/control/1"; +pub const ALPN_KV: &[u8] = b"/geth/kv/1"; +pub const ALPN_CAS: &[u8] = b"/geth/cas/1"; +pub const ALPN_PUBSUB: &[u8] = b"/geth/pubsub/1"; +pub const ALPN_PIPE: &[u8] = b"/geth/pipe/1"; +pub const ALPN_DB: &[u8] = b"/geth/db/1"; +pub const ALPN_DOCUMENT: &[u8] = b"/geth/document/1"; +pub const ALPN_SSH_PROXY: &[u8] = b"/geth/ssh-proxy/1"; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct EndpointStatus { + pub enabled: bool, + pub endpoint_id: Option, + pub note: String, +} + +impl EndpointStatus { + #[must_use] + pub fn scaffolded() -> Self { + Self { + enabled: false, + endpoint_id: None, + note: "Iroh endpoint integration is scaffolded for a later pinned API pass".to_owned(), + } + } +} diff --git a/crates/geth-keychain/Cargo.toml b/crates/geth-keychain/Cargo.toml new file mode 100644 index 0000000..6783bb5 --- /dev/null +++ b/crates/geth-keychain/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "geth-keychain" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +thiserror.workspace = true +geth-types = { path = "../geth-types" } + +[dev-dependencies] +serde_json.workspace = true diff --git a/crates/geth-keychain/src/lib.rs b/crates/geth-keychain/src/lib.rs new file mode 100644 index 0000000..b54ad46 --- /dev/null +++ b/crates/geth-keychain/src/lib.rs @@ -0,0 +1,137 @@ +use geth_types::{AgentId, DeviceId, KeyId, NodeId, UnixMillis, UserId}; +use serde::{Deserialize, Serialize}; + +pub const KEYCHAIN_SIGNATURE_NAMESPACE: &str = "geth.keychain.v1@geth.local"; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct SignedKeychainOp { + pub op: KeychainOp, + pub signer: KeyId, + pub signature_namespace: String, + pub signature: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct KeychainOp { + pub id: geth_types::AuthOpId, + pub created_at: UnixMillis, + pub kind: KeychainOpKind, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "kebab-case")] +pub enum KeychainOpKind { + KeychainInit, + AdminKeyAdd { + key: KeyId, + }, + AdminKeyRevoke { + key: KeyId, + }, + UserAdd { + user: UserId, + name: String, + }, + UserRename { + user: UserId, + name: String, + }, + UserRevoke { + user: UserId, + }, + DeviceAdd { + device: DeviceId, + user: UserId, + }, + DeviceRevoke { + device: DeviceId, + }, + DeviceKeyAdd { + device: DeviceId, + key: KeyId, + }, + DeviceKeyRevoke { + device: DeviceId, + key: KeyId, + }, + NodeAdd { + node: NodeId, + device: DeviceId, + name: String, + }, + NodeRename { + node: NodeId, + name: String, + }, + NodeRevoke { + node: NodeId, + }, + NodeEndpointAdd { + node: NodeId, + endpoint: String, + }, + NodeEndpointRevoke { + node: NodeId, + endpoint: String, + }, + AgentBind { + agent: AgentId, + node: NodeId, + }, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct KeychainView { + pub initialized: bool, + pub admin_keys: Vec, + pub users: Vec, + pub devices: Vec, + pub nodes: Vec, +} + +pub fn reduce_keychain_ops(ops: &[KeychainOp]) -> KeychainView { + let mut view = KeychainView::default(); + for op in ops { + match &op.kind { + KeychainOpKind::KeychainInit => view.initialized = true, + KeychainOpKind::AdminKeyAdd { key } if !view.admin_keys.contains(key) => { + view.admin_keys.push(key.clone()); + } + KeychainOpKind::AdminKeyRevoke { key } => view.admin_keys.retain(|item| item != key), + KeychainOpKind::UserAdd { user, .. } if !view.users.contains(user) => { + view.users.push(user.clone()); + } + KeychainOpKind::UserRevoke { user } => view.users.retain(|item| item != user), + KeychainOpKind::DeviceAdd { device, .. } if !view.devices.contains(device) => { + view.devices.push(device.clone()); + } + KeychainOpKind::DeviceRevoke { device } => view.devices.retain(|item| item != device), + KeychainOpKind::NodeAdd { node, .. } if !view.nodes.contains(node) => { + view.nodes.push(node.clone()); + } + KeychainOpKind::NodeRevoke { node } => view.nodes.retain(|item| item != node), + _ => {} + } + } + view +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn keychain_structs_roundtrip() { + let op = KeychainOp { + id: "op:1".into(), + created_at: UnixMillis(1), + kind: KeychainOpKind::UserAdd { + user: "user:eric".into(), + name: "Eric".to_owned(), + }, + }; + let json = serde_json::to_string(&op).expect("json"); + let decoded: KeychainOp = serde_json::from_str(&json).expect("decode"); + assert_eq!(decoded, op); + } +} diff --git a/crates/geth-kv/Cargo.toml b/crates/geth-kv/Cargo.toml new file mode 100644 index 0000000..6c91b92 --- /dev/null +++ b/crates/geth-kv/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "geth-kv" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +geth-types = { path = "../geth-types" } diff --git a/crates/geth-kv/src/lib.rs b/crates/geth-kv/src/lib.rs new file mode 100644 index 0000000..5d51c1e --- /dev/null +++ b/crates/geth-kv/src/lib.rs @@ -0,0 +1,14 @@ +use geth_types::{KvId, ResourceId}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct KvResource { + pub id: KvId, + pub resource: ResourceId, + pub name: String, +} + +#[must_use] +pub fn iroh_docs_roadmap() -> &'static str { + "future kv storage uses Iroh Documents namespaces with prefix-scoped authorization" +} diff --git a/crates/geth-node/Cargo.toml b/crates/geth-node/Cargo.toml new file mode 100644 index 0000000..69c5d45 --- /dev/null +++ b/crates/geth-node/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "geth-node" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde_json.workspace = true +thiserror.workspace = true +tokio.workspace = true +tracing.workspace = true +geth-auth = { path = "../geth-auth" } +geth-cas = { path = "../geth-cas" } +geth-config = { path = "../geth-config" } +geth-control = { path = "../geth-control" } +geth-crypto = { path = "../geth-crypto" } +geth-resource = { path = "../geth-resource" } +geth-store = { path = "../geth-store" } +geth-types = { path = "../geth-types" } diff --git a/crates/geth-node/src/lib.rs b/crates/geth-node/src/lib.rs new file mode 100644 index 0000000..79b07fa --- /dev/null +++ b/crates/geth-node/src/lib.rs @@ -0,0 +1,244 @@ +use geth_auth::AuthExplanation; +use geth_cas::{LocalCas, hash_path}; +use geth_config::GethPaths; +use geth_control::{ + CasBlob, ControlRequest, ControlResponse, KeychainStatusResponse, NodeIdResponse, + StatusResponse, +}; +use geth_crypto::AgentKey; +use geth_resource::ResourceDescriptor; +use geth_store::{Store, StoredResource}; +use geth_types::{ResourceId, ResourceKind, ResourceName}; +use std::path::Path; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::{UnixListener, UnixStream}; + +#[derive(Debug, thiserror::Error)] +pub enum NodeError { + #[error("config error: {0}")] + Config(#[from] geth_config::ConfigError), + #[error("crypto error: {0}")] + Crypto(#[from] geth_crypto::CryptoError), + #[error("store error: {0}")] + Store(#[from] geth_store::StoreError), + #[error("cas error: {0}")] + Cas(#[from] geth_cas::CasError), + #[error("control error: {0}")] + Control(#[from] geth_control::ControlError), + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("invalid resource kind: {0}")] + InvalidResourceKind(String), +} + +#[derive(Clone, Debug)] +pub struct LocalNode { + pub paths: GethPaths, + pub agent_id: String, + pub node_id: String, +} + +pub fn init_node(paths: &GethPaths) -> Result { + paths.ensure_base_dirs()?; + if !paths.config_file().exists() { + std::fs::write( + paths.config_file(), + "# geth local node config\n# Remote node-to-node communication is Iroh-only.\n", + )?; + } + let key = AgentKey::load_or_create(&paths.agent_key())?; + let agent_id = key.agent_id().to_string(); + let node_id = stable_node_id(&agent_id); + let store = Store::open(&paths.metadata_db())?; + store.upsert_agent(&agent_id, &key.public_key_hex())?; + store.upsert_node(&node_id, "local", &agent_id)?; + store.insert_resource(&StoredResource { + resource_id: "resource:cas:local".to_owned(), + kind: ResourceKind::Cas.to_string(), + name: "local-cas".to_owned(), + status: "active".to_owned(), + })?; + Ok(LocalNode { + paths: paths.clone(), + agent_id, + node_id, + }) +} + +pub fn open_node(paths: &GethPaths) -> Result { + init_node(paths) +} + +pub async fn run_daemon(paths: GethPaths) -> Result<(), NodeError> { + let node = init_node(&paths)?; + if Path::new(&paths.socket_path()).exists() { + std::fs::remove_file(paths.socket_path())?; + } + let listener = UnixListener::bind(paths.socket_path())?; + tracing::info!(socket = %paths.socket_path().display(), "geth daemon listening"); + + loop { + let (stream, _) = listener.accept().await?; + let node = node.clone(); + tokio::spawn(async move { + if let Err(error) = handle_stream(node, stream).await { + tracing::warn!(%error, "control request failed"); + } + }); + } +} + +pub async fn send_control( + paths: &GethPaths, + request: ControlRequest, +) -> Result { + let mut stream = UnixStream::connect(paths.socket_path()).await?; + stream + .write_all(geth_control::encode_request(&request)?.as_bytes()) + .await?; + stream.shutdown().await?; + let mut reader = BufReader::new(stream); + let mut line = String::new(); + reader.read_line(&mut line).await?; + Ok(geth_control::decode_response(&line)?) +} + +async fn handle_stream(node: LocalNode, stream: UnixStream) -> Result<(), NodeError> { + let mut reader = BufReader::new(stream); + let mut line = String::new(); + reader.read_line(&mut line).await?; + let request = geth_control::decode_request(&line)?; + let response = match handle_request(&node, request) { + Ok(response) => response, + Err(error) => ControlResponse::Error { + message: error.to_string(), + }, + }; + let mut stream = reader.into_inner(); + stream + .write_all(geth_control::encode_response(&response)?.as_bytes()) + .await?; + Ok(()) +} + +pub fn handle_request( + node: &LocalNode, + request: ControlRequest, +) -> Result { + let store = Store::open(&node.paths.metadata_db())?; + match request { + ControlRequest::Status => Ok(ControlResponse::Status(StatusResponse { + home: node.paths.home().to_path_buf(), + socket: node.paths.socket_path(), + agent_id: node.agent_id.clone(), + node_id: node.node_id.clone(), + iroh: "scaffolded; no remote endpoint is started in bootstrap".to_owned(), + })), + ControlRequest::NodeId => Ok(ControlResponse::NodeId(NodeIdResponse { + agent_id: node.agent_id.clone(), + node_id: node.node_id.clone(), + endpoint_id: None, + })), + ControlRequest::ResourceList => Ok(ControlResponse::ResourceList { + resources: store + .list_resources()? + .into_iter() + .map(stored_resource_to_descriptor) + .collect::, _>>()?, + }), + ControlRequest::ResourceCreate { kind, name } => { + let kind = kind + .parse::() + .map_err(|_| NodeError::InvalidResourceKind(kind.clone()))?; + let id = format!("resource:{}:{}", kind, name); + let stored = StoredResource { + resource_id: id, + kind: kind.to_string(), + name, + status: "active".to_owned(), + }; + store.insert_resource(&stored)?; + Ok(ControlResponse::ResourceCreated { + resource: stored_resource_to_descriptor(stored)?, + }) + } + ControlRequest::CasAdd { path } => { + let cas = LocalCas::new(node.paths.cas_dir()); + let info = cas.add_path(&path)?; + store.record_cas_object( + info.hash.as_str(), + info.size_bytes, + &info.path.to_string_lossy(), + )?; + Ok(ControlResponse::CasAdded { + hash: info.hash, + size_bytes: info.size_bytes, + }) + } + ControlRequest::CasGet { hash, out } => { + let cas = LocalCas::new(node.paths.cas_dir()); + let size_bytes = cas.get_to_path(&hash, &out)?; + Ok(ControlResponse::CasGot { + hash, + out, + size_bytes, + }) + } + ControlRequest::CasHash { path } => Ok(ControlResponse::CasHash { + hash: hash_path(&path)?, + }), + ControlRequest::CasHas { hash } => { + let cas = LocalCas::new(node.paths.cas_dir()); + let present = cas.has(&hash)?; + Ok(ControlResponse::CasHas { hash, present }) + } + ControlRequest::CasList => { + let cas = LocalCas::new(node.paths.cas_dir()); + Ok(ControlResponse::CasList { + blobs: cas + .list()? + .into_iter() + .map(|blob| CasBlob { + hash: blob.hash, + size_bytes: blob.size_bytes, + }) + .collect(), + }) + } + ControlRequest::KeychainStatus => { + Ok(ControlResponse::KeychainStatus(KeychainStatusResponse { + initialized: false, + admin_keys: 0, + users: 0, + devices: 0, + nodes: 1, + })) + } + ControlRequest::AuthExplain { + subject, + resource, + capability, + } => Ok(ControlResponse::AuthExplain(AuthExplanation::stub( + subject, resource, capability, + ))), + ControlRequest::ModuleStub { module, command } => { + Ok(ControlResponse::NotImplemented { module, command }) + } + } +} + +fn stored_resource_to_descriptor(stored: StoredResource) -> Result { + let kind = stored + .kind + .parse::() + .map_err(|_| NodeError::InvalidResourceKind(stored.kind.clone()))?; + Ok(ResourceDescriptor::local( + ResourceId::new(stored.resource_id), + kind, + ResourceName::new(stored.name), + )) +} + +fn stable_node_id(agent_id: &str) -> String { + format!("node:{agent_id}") +} diff --git a/crates/geth-pipe/Cargo.toml b/crates/geth-pipe/Cargo.toml new file mode 100644 index 0000000..9bcefed --- /dev/null +++ b/crates/geth-pipe/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "geth-pipe" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +geth-types = { path = "../geth-types" } diff --git a/crates/geth-pipe/src/lib.rs b/crates/geth-pipe/src/lib.rs new file mode 100644 index 0000000..6f7a60c --- /dev/null +++ b/crates/geth-pipe/src/lib.rs @@ -0,0 +1,14 @@ +use geth_types::{PipeId, ResourceId}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PipeResource { + pub id: PipeId, + pub resource: ResourceId, + pub name: String, +} + +#[must_use] +pub fn pipe_roadmap() -> &'static str { + "future pipes are authorized Iroh bidirectional streams for stdin/stdout and forwarding" +} diff --git a/crates/geth-pubsub/Cargo.toml b/crates/geth-pubsub/Cargo.toml new file mode 100644 index 0000000..0f110cf --- /dev/null +++ b/crates/geth-pubsub/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "geth-pubsub" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +geth-types = { path = "../geth-types" } diff --git a/crates/geth-pubsub/src/lib.rs b/crates/geth-pubsub/src/lib.rs new file mode 100644 index 0000000..fe14b55 --- /dev/null +++ b/crates/geth-pubsub/src/lib.rs @@ -0,0 +1,14 @@ +use geth_types::{ResourceId, TopicId}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PubsubTopic { + pub id: TopicId, + pub resource: ResourceId, + pub name: String, +} + +#[must_use] +pub fn pubsub_storage_warning() -> &'static str { + "pubsub is lossy notification transport, not authoritative storage" +} diff --git a/crates/geth-resource/Cargo.toml b/crates/geth-resource/Cargo.toml new file mode 100644 index 0000000..08b82bf --- /dev/null +++ b/crates/geth-resource/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "geth-resource" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +thiserror.workspace = true +geth-types = { path = "../geth-types" } + +[dev-dependencies] +serde_json.workspace = true diff --git a/crates/geth-resource/src/lib.rs b/crates/geth-resource/src/lib.rs new file mode 100644 index 0000000..adc0eee --- /dev/null +++ b/crates/geth-resource/src/lib.rs @@ -0,0 +1,88 @@ +use geth_types::{ResourceId, ResourceKind, ResourceName}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ResourceDescriptor { + pub id: ResourceId, + pub kind: ResourceKind, + pub name: ResourceName, + pub authority: ResourceAuthorityRef, + pub local_role: LocalRole, + pub replication: ReplicationPolicy, + pub retention: RetentionPolicy, + pub status: ResourceStatus, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", content = "value", rename_all = "kebab-case")] +pub enum ResourceAuthorityRef { + Local, + Resource(ResourceId), + External(String), +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum LocalRole { + Owner, + Replica, + Cache, + Stub, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ReplicationPolicy { + LocalOnly, + Manual, + Mesh, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum RetentionPolicy { + Keep, + Cache, + Ephemeral, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ResourceStatus { + Active, + Stub, + Revoked, +} + +impl ResourceDescriptor { + #[must_use] + pub fn local(id: ResourceId, kind: ResourceKind, name: ResourceName) -> Self { + Self { + id, + kind, + name, + authority: ResourceAuthorityRef::Local, + local_role: LocalRole::Owner, + replication: ReplicationPolicy::LocalOnly, + retention: RetentionPolicy::Keep, + status: ResourceStatus::Active, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resource_descriptor_serializes() { + let descriptor = ResourceDescriptor::local( + ResourceId::new("resource:notes"), + ResourceKind::Kv, + ResourceName::new("notes"), + ); + let json = serde_json::to_string(&descriptor).expect("json"); + let decoded: ResourceDescriptor = serde_json::from_str(&json).expect("decode"); + assert_eq!(decoded, descriptor); + } +} diff --git a/crates/geth-secrets/Cargo.toml b/crates/geth-secrets/Cargo.toml new file mode 100644 index 0000000..85f08af --- /dev/null +++ b/crates/geth-secrets/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "geth-secrets" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +thiserror.workspace = true +geth-types = { path = "../geth-types" } diff --git a/crates/geth-secrets/src/lib.rs b/crates/geth-secrets/src/lib.rs new file mode 100644 index 0000000..a40034e --- /dev/null +++ b/crates/geth-secrets/src/lib.rs @@ -0,0 +1,47 @@ +use geth_types::{Capability, PrincipalId, ResourceId, SecretId, UnixMillis}; +use serde::{Deserialize, Serialize}; + +pub const RESOURCE_SECRET_SIGNATURE_NAMESPACE: &str = "geth.resource-secret.v1@geth.local"; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ResourceMasterSecret { + pub id: SecretId, + pub resource: ResourceId, + pub epoch: u64, + pub created_at: UnixMillis, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ResourceKeyEnvelope { + pub secret: SecretId, + pub recipient: PrincipalId, + pub epoch: u64, + pub algorithm: String, + pub ciphertext: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BearerAccess { + pub secret: SecretId, + pub resource: ResourceId, + pub capabilities: Vec, + pub expires_at: Option, + pub may_delegate: bool, +} + +impl BearerAccess { + #[must_use] + pub fn resource_scoped( + secret: SecretId, + resource: ResourceId, + capabilities: Vec, + ) -> Self { + Self { + secret, + resource, + capabilities, + expires_at: None, + may_delegate: false, + } + } +} diff --git a/crates/geth-ssh-identity/Cargo.toml b/crates/geth-ssh-identity/Cargo.toml new file mode 100644 index 0000000..b9b8bb9 --- /dev/null +++ b/crates/geth-ssh-identity/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "geth-ssh-identity" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +thiserror.workspace = true diff --git a/crates/geth-ssh-identity/src/lib.rs b/crates/geth-ssh-identity/src/lib.rs new file mode 100644 index 0000000..3914a18 --- /dev/null +++ b/crates/geth-ssh-identity/src/lib.rs @@ -0,0 +1,40 @@ +use std::path::Path; +use std::process::Command; + +pub const KEYCHAIN_NAMESPACE: &str = "geth.keychain.v1@geth.local"; +pub const AUTH_OP_NAMESPACE: &str = "geth.auth-op.v1@geth.local"; +pub const RESOURCE_GRANT_NAMESPACE: &str = "geth.resource-grant.v1@geth.local"; +pub const RESOURCE_SECRET_NAMESPACE: &str = "geth.resource-secret.v1@geth.local"; +pub const REVOCATION_NAMESPACE: &str = "geth.revocation.v1@geth.local"; + +#[derive(Debug, thiserror::Error)] +pub enum SshIdentityError { + #[error("ssh-keygen failed or is unavailable")] + SshKeygenUnavailable, + #[error("io error: {0}")] + Io(#[from] std::io::Error), +} + +pub fn ensure_ssh_keygen_available() -> Result<(), SshIdentityError> { + let status = Command::new("ssh-keygen").arg("-?").status(); + match status { + Ok(_) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + Err(SshIdentityError::SshKeygenUnavailable) + } + Err(error) => Err(SshIdentityError::Io(error)), + } +} + +pub fn sign_command(key_path: &Path, namespace: &str, input_path: &Path) -> Command { + let mut command = Command::new("ssh-keygen"); + command + .arg("-Y") + .arg("sign") + .arg("-f") + .arg(key_path) + .arg("-n") + .arg(namespace) + .arg(input_path); + command +} diff --git a/crates/geth-ssh-proxy/Cargo.toml b/crates/geth-ssh-proxy/Cargo.toml new file mode 100644 index 0000000..bbb40ed --- /dev/null +++ b/crates/geth-ssh-proxy/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "geth-ssh-proxy" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +geth-types = { path = "../geth-types" } diff --git a/crates/geth-ssh-proxy/src/lib.rs b/crates/geth-ssh-proxy/src/lib.rs new file mode 100644 index 0000000..a6a4c39 --- /dev/null +++ b/crates/geth-ssh-proxy/src/lib.rs @@ -0,0 +1,13 @@ +use geth_types::{NodeId, ResourceId}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct SshProxyTarget { + pub resource: ResourceId, + pub node: NodeId, +} + +#[must_use] +pub fn ssh_proxy_roadmap() -> &'static str { + "future SSH proxy carries SSH protocol bytes over authorized Iroh streams; SSH is not a geth transport" +} diff --git a/crates/geth-store/Cargo.toml b/crates/geth-store/Cargo.toml new file mode 100644 index 0000000..0371973 --- /dev/null +++ b/crates/geth-store/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "geth-store" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +rusqlite.workspace = true +serde_json.workspace = true +thiserror.workspace = true +time.workspace = true +geth-types = { path = "../geth-types" } diff --git a/crates/geth-store/src/lib.rs b/crates/geth-store/src/lib.rs new file mode 100644 index 0000000..4fb8613 --- /dev/null +++ b/crates/geth-store/src/lib.rs @@ -0,0 +1,240 @@ +use rusqlite::{Connection, params}; +use std::path::Path; + +#[derive(Debug, thiserror::Error)] +pub enum StoreError { + #[error("sqlite error: {0}")] + Sqlite(#[from] rusqlite::Error), + #[error("json error: {0}")] + Json(#[from] serde_json::Error), +} + +pub struct Store { + conn: Connection, +} + +impl Store { + pub fn open(path: &Path) -> Result { + let conn = Connection::open(path)?; + let store = Self { conn }; + store.migrate()?; + Ok(store) + } + + pub fn open_memory() -> Result { + let conn = Connection::open_in_memory()?; + let store = Self { conn }; + store.migrate()?; + Ok(store) + } + + pub fn migrate(&self) -> Result<(), StoreError> { + self.conn.execute_batch( + r#" + PRAGMA foreign_keys = ON; + CREATE TABLE IF NOT EXISTS meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS agents ( + agent_id TEXT PRIMARY KEY, + public_key TEXT NOT NULL, + created_at_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS nodes ( + node_id TEXT PRIMARY KEY, + name TEXT NOT NULL, + agent_id TEXT, + created_at_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS resources ( + resource_id TEXT PRIMARY KEY, + kind TEXT NOT NULL, + name TEXT NOT NULL, + authority_ref TEXT NOT NULL, + local_role TEXT NOT NULL, + replication_policy TEXT NOT NULL, + retention_policy TEXT NOT NULL, + status TEXT NOT NULL, + created_at_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS keychain_ops ( + op_id TEXT PRIMARY KEY, + op_json TEXT NOT NULL, + created_at_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS auth_ops ( + op_id TEXT PRIMARY KEY, + resource_id TEXT NOT NULL, + op_json TEXT NOT NULL, + created_at_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS grants ( + grant_id TEXT PRIMARY KEY, + resource_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + capability TEXT NOT NULL, + revoked INTEGER NOT NULL DEFAULT 0 + ); + CREATE TABLE IF NOT EXISTS resource_secrets ( + secret_id TEXT PRIMARY KEY, + resource_id TEXT NOT NULL, + epoch INTEGER NOT NULL, + status TEXT NOT NULL, + created_at_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS cas_objects ( + hash TEXT PRIMARY KEY, + size_bytes INTEGER NOT NULL, + path TEXT NOT NULL, + created_at_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS cas_pins ( + hash TEXT PRIMARY KEY, + pinned_at_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS kv_stores ( + kv_id TEXT PRIMARY KEY, + resource_id TEXT NOT NULL, + name TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS db_resources ( + db_id TEXT PRIMARY KEY, + resource_id TEXT NOT NULL, + path TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS document_resources ( + document_id TEXT PRIMARY KEY, + resource_id TEXT NOT NULL, + name TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS peer_cards ( + peer_id TEXT PRIMARY KEY, + card_json TEXT NOT NULL, + updated_at_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS module_state ( + module TEXT PRIMARY KEY, + state_json TEXT NOT NULL, + updated_at_ms INTEGER NOT NULL + ); + INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', '1'); + "#, + )?; + Ok(()) + } + + pub fn upsert_agent(&self, agent_id: &str, public_key: &str) -> Result<(), StoreError> { + self.conn.execute( + "INSERT OR IGNORE INTO agents(agent_id, public_key, created_at_ms) VALUES (?1, ?2, ?3)", + params![agent_id, public_key, now_ms()], + )?; + Ok(()) + } + + pub fn upsert_node(&self, node_id: &str, name: &str, agent_id: &str) -> Result<(), StoreError> { + self.conn.execute( + "INSERT OR IGNORE INTO nodes(node_id, name, agent_id, created_at_ms) VALUES (?1, ?2, ?3, ?4)", + params![node_id, name, agent_id, now_ms()], + )?; + Ok(()) + } + + pub fn list_resources(&self) -> Result, StoreError> { + let mut stmt = self.conn.prepare( + "SELECT resource_id, kind, name, status FROM resources ORDER BY kind, name, resource_id", + )?; + let rows = stmt.query_map([], |row| { + Ok(StoredResource { + resource_id: row.get(0)?, + kind: row.get(1)?, + name: row.get(2)?, + status: row.get(3)?, + }) + })?; + rows.collect::, _>>() + .map_err(StoreError::from) + } + + pub fn insert_resource(&self, resource: &StoredResource) -> Result<(), StoreError> { + self.conn.execute( + r#"INSERT OR IGNORE INTO resources( + resource_id, kind, name, authority_ref, local_role, replication_policy, + retention_policy, status, created_at_ms + ) VALUES (?1, ?2, ?3, 'local', 'owner', 'local-only', 'keep', ?4, ?5)"#, + params![ + resource.resource_id, + resource.kind, + resource.name, + resource.status, + now_ms() + ], + )?; + Ok(()) + } + + pub fn record_cas_object( + &self, + hash: &str, + size_bytes: u64, + path: &str, + ) -> Result<(), StoreError> { + self.conn.execute( + "INSERT OR REPLACE INTO cas_objects(hash, size_bytes, path, created_at_ms) VALUES (?1, ?2, ?3, ?4)", + params![hash, size_bytes as i64, path, now_ms()], + )?; + Ok(()) + } + + pub fn list_cas_objects(&self) -> Result, StoreError> { + let mut stmt = self.conn.prepare( + "SELECT hash, size_bytes, path FROM cas_objects ORDER BY created_at_ms, hash", + )?; + let rows = stmt.query_map([], |row| { + Ok(CasObject { + hash: row.get(0)?, + size_bytes: row.get::<_, i64>(1)? as u64, + path: row.get(2)?, + }) + })?; + rows.collect::, _>>() + .map_err(StoreError::from) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StoredResource { + pub resource_id: String, + pub kind: String, + pub name: String, + pub status: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CasObject { + pub hash: String, + pub size_bytes: u64, + pub path: String, +} + +#[must_use] +pub fn now_ms() -> i64 { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default(); + i64::try_from(now.as_millis()).unwrap_or(i64::MAX) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn migrations_are_idempotent() { + let store = Store::open_memory().expect("open"); + store.migrate().expect("migrate again"); + store.migrate().expect("migrate third time"); + let resources = store.list_resources().expect("resources"); + assert!(resources.is_empty()); + } +} diff --git a/crates/geth-testkit/Cargo.toml b/crates/geth-testkit/Cargo.toml new file mode 100644 index 0000000..1595d6b --- /dev/null +++ b/crates/geth-testkit/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "geth-testkit" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +tempfile.workspace = true +geth-config = { path = "../geth-config" } +geth-node = { path = "../geth-node" } diff --git a/crates/geth-testkit/src/lib.rs b/crates/geth-testkit/src/lib.rs new file mode 100644 index 0000000..5a1efc6 --- /dev/null +++ b/crates/geth-testkit/src/lib.rs @@ -0,0 +1,14 @@ +use tempfile::TempDir; + +pub struct TestHome { + _dir: TempDir, + pub paths: geth_config::GethPaths, +} + +impl TestHome { + pub fn new() -> std::io::Result { + let dir = tempfile::tempdir()?; + let paths = geth_config::GethPaths::from_home(dir.path()); + Ok(Self { _dir: dir, paths }) + } +} diff --git a/crates/geth-types/Cargo.toml b/crates/geth-types/Cargo.toml new file mode 100644 index 0000000..8d5c6e6 --- /dev/null +++ b/crates/geth-types/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "geth-types" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +thiserror.workspace = true diff --git a/crates/geth-types/src/lib.rs b/crates/geth-types/src/lib.rs new file mode 100644 index 0000000..ffd26c1 --- /dev/null +++ b/crates/geth-types/src/lib.rs @@ -0,0 +1,132 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +macro_rules! string_id { + ($name:ident) => { + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] + pub struct $name(String); + + impl $name { + #[must_use] + pub fn new(value: impl Into) -> Self { + Self(value.into()) + } + + #[must_use] + pub fn as_str(&self) -> &str { + &self.0 + } + } + + impl Display for $name { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } + } + + impl From for $name { + fn from(value: String) -> Self { + Self(value) + } + } + + impl From<&str> for $name { + fn from(value: &str) -> Self { + Self(value.to_owned()) + } + } + }; +} + +string_id!(AgentId); +string_id!(UserId); +string_id!(DeviceId); +string_id!(NodeId); +string_id!(MemberId); +string_id!(GroupId); +string_id!(ResourceId); +string_id!(ResourceName); +string_id!(PrincipalId); +string_id!(Capability); +string_id!(BlobHash); +string_id!(DocumentId); +string_id!(KvId); +string_id!(DbId); +string_id!(PipeId); +string_id!(TopicId); +string_id!(AuthOpId); +string_id!(KeyId); +string_id!(SecretId); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct UnixMillis(pub i64); + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ResourceKind { + Db, + Kv, + Pipe, + Document, + Pubsub, + Cas, + SshProxy, +} + +impl ResourceKind { + #[must_use] + pub fn as_str(&self) -> &'static str { + match self { + Self::Db => "db", + Self::Kv => "kv", + Self::Pipe => "pipe", + Self::Document => "document", + Self::Pubsub => "pubsub", + Self::Cas => "cas", + Self::SshProxy => "ssh-proxy", + } + } +} + +impl Display for ResourceKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +impl std::str::FromStr for ResourceKind { + type Err = TypeParseError; + + fn from_str(value: &str) -> Result { + match value { + "db" => Ok(Self::Db), + "kv" => Ok(Self::Kv), + "pipe" => Ok(Self::Pipe), + "document" => Ok(Self::Document), + "pubsub" => Ok(Self::Pubsub), + "cas" => Ok(Self::Cas), + "ssh-proxy" | "ssh" => Ok(Self::SshProxy), + _ => Err(TypeParseError::UnknownResourceKind(value.to_owned())), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum TypeParseError { + #[error("unknown resource kind: {0}")] + UnknownResourceKind(String), +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(tag = "kind", content = "id", rename_all = "kebab-case")] +pub enum Principal { + AdminKey(KeyId), + User(UserId), + Device(DeviceId), + Node(NodeId), + Agent(AgentId), + IrohEndpoint(String), + Group(GroupId), + Resource(ResourceId), + BearerSecret(SecretId), +} diff --git a/crates/geth/Cargo.toml b/crates/geth/Cargo.toml new file mode 100644 index 0000000..ff55137 --- /dev/null +++ b/crates/geth/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "geth" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[[bin]] +name = "geth" +path = "src/main.rs" + +[dependencies] +anyhow.workspace = true +tokio.workspace = true +tracing-subscriber.workspace = true +geth-cli = { path = "../geth-cli" } + +[dev-dependencies] +geth-cas = { path = "../geth-cas" } +geth-config = { path = "../geth-config" } +geth-node = { path = "../geth-node" } +tempfile.workspace = true diff --git a/crates/geth/src/main.rs b/crates/geth/src/main.rs new file mode 100644 index 0000000..2652df8 --- /dev/null +++ b/crates/geth/src/main.rs @@ -0,0 +1,8 @@ +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .with_target(false) + .init(); + geth_cli::run().await +} diff --git a/crates/geth/tests/bootstrap.rs b/crates/geth/tests/bootstrap.rs new file mode 100644 index 0000000..69f9847 --- /dev/null +++ b/crates/geth/tests/bootstrap.rs @@ -0,0 +1,105 @@ +use std::process::{Child, Command}; +use std::time::{Duration, Instant}; + +fn unix_sockets_available(home: &std::path::Path) -> bool { + let probe = home.join("probe.sock"); + match std::os::unix::net::UnixListener::bind(&probe) { + Ok(listener) => { + drop(listener); + let _ = std::fs::remove_file(probe); + true + } + Err(error) => { + eprintln!("skipping daemon socket test; Unix sockets unavailable: {error}"); + false + } + } +} + +fn geth_bin() -> &'static str { + env!("CARGO_BIN_EXE_geth") +} + +fn run_geth(home: &std::path::Path, args: &[&str]) -> std::process::Output { + Command::new(geth_bin()) + .env("GETH_HOME", home) + .args(args) + .output() + .expect("run geth") +} + +fn spawn_daemon(home: &std::path::Path) -> Child { + Command::new(geth_bin()) + .env("GETH_HOME", home) + .args(["daemon", "run"]) + .spawn() + .expect("spawn daemon") +} + +fn wait_for_socket(path: &std::path::Path) { + let started = Instant::now(); + while started.elapsed() < Duration::from_secs(5) { + if path.exists() { + return; + } + std::thread::sleep(Duration::from_millis(50)); + } + panic!("socket did not appear: {}", path.display()); +} + +#[test] +fn geth_init_in_temp_home() { + let home = tempfile::tempdir().expect("tempdir"); + let output = run_geth(home.path(), &["init"]); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(home.path().join("geth.sqlite").exists()); + assert!(home.path().join("identity/agent.ed25519").exists()); + assert!(home.path().join("config.toml").exists()); +} + +#[test] +fn geth_status_against_running_daemon() { + let home = tempfile::tempdir().expect("tempdir"); + if !unix_sockets_available(home.path()) { + return; + } + assert!(run_geth(home.path(), &["init"]).status.success()); + let mut daemon = spawn_daemon(home.path()); + wait_for_socket(&home.path().join("run/geth.sock")); + + let output = run_geth(home.path(), &["status"]); + let _ = daemon.kill(); + let _ = daemon.wait(); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("geth daemon: running")); + assert!(stdout.contains("agent:")); +} + +#[test] +fn initialized_node_can_roundtrip_cas_blob() { + let home = tempfile::tempdir().expect("tempdir"); + let paths = geth_config::GethPaths::from_home(home.path()); + let node = geth_node::init_node(&paths).expect("init node"); + assert_eq!(node.paths.home(), home.path()); + + let cas = geth_cas::LocalCas::new(paths.cas_dir()); + let output_path = home.path().join("output.txt"); + let added = cas.add_bytes(b"hello geth integration").expect("add blob"); + assert!(cas.has(&added.hash).expect("has blob")); + cas.get_to_path(&added.hash, &output_path) + .expect("get blob"); + assert_eq!( + std::fs::read(output_path).expect("read output"), + b"hello geth integration" + ); +} diff --git a/docs/adr/0000-project-goals.md b/docs/adr/0000-project-goals.md new file mode 100644 index 0000000..8ade5ed --- /dev/null +++ b/docs/adr/0000-project-goals.md @@ -0,0 +1,18 @@ +# ADR 0000: Project Goals + +## Status + +Accepted. + +## Decision + +`geth` is a personal, local-first, script-friendly mesh runtime built as a Rust +monorepo around Iroh networking and resource-oriented modules. + +It is not a blockchain, not a consensus system, not the Ethereum geth client, +and not a replacement for OpenSSH authentication. + +## Consequences + +The project favors local ownership, explicit resources, durable metadata, +capability-based authorization, and future multi-user collaboration. diff --git a/docs/adr/0001-single-binary-cli-and-daemon.md b/docs/adr/0001-single-binary-cli-and-daemon.md new file mode 100644 index 0000000..e74a21a --- /dev/null +++ b/docs/adr/0001-single-binary-cli-and-daemon.md @@ -0,0 +1,18 @@ +# ADR 0001: Single Binary CLI And Daemon + +## Status + +Accepted. + +## Decision + +The project ships one executable: `geth`. It has daemon mode with +`geth daemon run` and control mode through commands such as `geth status`. + +Control commands use a local Unix socket. There are no separate `gethd` or +`gethctl` binaries. + +## Consequences + +Packaging and user mental model stay simple. The daemon remains the owner of +identity, Iroh endpoint state, resources, and synchronized data structures. diff --git a/docs/adr/0002-iroh-only-remote-transport.md b/docs/adr/0002-iroh-only-remote-transport.md new file mode 100644 index 0000000..68e4740 --- /dev/null +++ b/docs/adr/0002-iroh-only-remote-transport.md @@ -0,0 +1,17 @@ +# ADR 0002: Iroh-Only Remote Transport + +## Status + +Accepted. + +## Decision + +All remote geth node-to-node communication happens over Iroh. The daemon will +own one shared endpoint and register module protocols on explicit ALPNs. + +There is no SSH transport fallback and no `ssh host gethd stdio` flow. + +## Consequences + +Transport behavior is coherent and resource authorization can be enforced before +opening module streams. diff --git a/docs/adr/0003-resource-model.md b/docs/adr/0003-resource-model.md new file mode 100644 index 0000000..960cc86 --- /dev/null +++ b/docs/adr/0003-resource-model.md @@ -0,0 +1,18 @@ +# ADR 0003: Resource Model + +## Status + +Accepted. + +## Decision + +Everything meaningful is modeled as a resource. Initial kinds are `db`, `kv`, +`pipe`, `document`, `pubsub`, `cas`, and `ssh-proxy`. + +Resources have capability-scoped authorization. Roles may exist as bundles, but +capabilities are the underlying permission unit. + +## Consequences + +Resource APIs, auth logs, secrets, and future replication can share one model +without relying on global roles. diff --git a/docs/adr/0004-keychain-and-auth-model.md b/docs/adr/0004-keychain-and-auth-model.md new file mode 100644 index 0000000..445c34f --- /dev/null +++ b/docs/adr/0004-keychain-and-auth-model.md @@ -0,0 +1,18 @@ +# ADR 0004: Keychain And Auth Model + +## Status + +Accepted. + +## Decision + +The keychain is SSH-rooted and models admin keys, users, devices, nodes, agents, +and endpoint bindings. Resource authorization is represented as signed operation +logs reduced into a current permissions view. + +Mutable ACL blobs are not the primary source of truth. + +## Consequences + +The system can answer why an operation is allowed or denied and can evolve toward +local-first multi-user authorization. diff --git a/docs/adr/0005-bearer-resource-secrets.md b/docs/adr/0005-bearer-resource-secrets.md new file mode 100644 index 0000000..82a7610 --- /dev/null +++ b/docs/adr/0005-bearer-resource-secrets.md @@ -0,0 +1,18 @@ +# ADR 0005: Bearer Resource Secrets + +## Status + +Accepted. + +## Decision + +Bearer secrets may grant resource-specific capabilities. They do not create +trusted node identity and should not normally grant trust graph mutation rights. + +Private resource access is modeled with resource master secret epochs. Revocation +is approximated by rotating to a new epoch and distributing it only to remaining +authorized devices. + +## Consequences + +Invites and temporary access can be practical without weakening node identity. diff --git a/docs/adr/0006-keyhive-beekem-roadmap.md b/docs/adr/0006-keyhive-beekem-roadmap.md new file mode 100644 index 0000000..f2fe10e --- /dev/null +++ b/docs/adr/0006-keyhive-beekem-roadmap.md @@ -0,0 +1,16 @@ +# ADR 0006: Keyhive/BeeKEM Roadmap + +## Status + +Accepted. + +## Decision + +The project will leave room for Keyhive-like local-first authorization and +BeeKEM/CGKA-style group key evolution later. The bootstrap does not implement +BeeKEM and does not claim strong forward secrecy or post-compromise security. + +## Consequences + +Types are versioned and capability-oriented, while advanced cryptography remains +out of scope for the MVP. diff --git a/docs/adr/0007-synchronized-data-structures.md b/docs/adr/0007-synchronized-data-structures.md new file mode 100644 index 0000000..fec6742 --- /dev/null +++ b/docs/adr/0007-synchronized-data-structures.md @@ -0,0 +1,15 @@ +# ADR 0007: Synchronized Data Structures + +## Status + +Accepted. + +## Decision + +The planned synchronized structures are `db`, `kv`, `pipe`, `document`, +`pubsub`, `cas`, and `ssh-proxy`. + +## Consequences + +Each module can progress independently while sharing resource registration, +capability checks, daemon-owned Iroh endpoint access, and local metadata. diff --git a/docs/adr/0008-cas-and-blob-distribution.md b/docs/adr/0008-cas-and-blob-distribution.md new file mode 100644 index 0000000..98e36a0 --- /dev/null +++ b/docs/adr/0008-cas-and-blob-distribution.md @@ -0,0 +1,16 @@ +# ADR 0008: CAS And Blob Distribution + +## Status + +Accepted. + +## Decision + +CAS starts as local filesystem storage with BLAKE3 content hashes. Future work +adds iroh-blobs, provider tracking, pins, encrypted private blobs, collections, +manifests, and file sync tree objects. + +## Consequences + +The MVP has useful local blob commands while preserving a path to networked blob +distribution. diff --git a/docs/adr/0009-ssh-trust-and-ssh-proxy.md b/docs/adr/0009-ssh-trust-and-ssh-proxy.md new file mode 100644 index 0000000..ad842d6 --- /dev/null +++ b/docs/adr/0009-ssh-trust-and-ssh-proxy.md @@ -0,0 +1,16 @@ +# ADR 0009: SSH Trust And SSH Proxy + +## Status + +Accepted. + +## Decision + +SSH keys are admin signing identities and ecosystem integration points. SSH is +not a geth transport. Future SSH proxy support will carry SSH protocol bytes over +authorized Iroh streams, and OpenSSH will still perform normal login auth. + +## Consequences + +Knowing an Iroh EndpointID is insufficient to reach sshd. Geth authorization must +allow `ssh_proxy.connect` before any SSH/admin endpoint is opened. diff --git a/docs/adr/0010-crsqlite-db-sync.md b/docs/adr/0010-crsqlite-db-sync.md new file mode 100644 index 0000000..b994d73 --- /dev/null +++ b/docs/adr/0010-crsqlite-db-sync.md @@ -0,0 +1,16 @@ +# ADR 0010: cr-sqlite DB Sync + +## Status + +Accepted. + +## Decision + +Database sync will use local SQLite databases with cr-sqlite. Changes from +`crsql_changes` will be exchanged over Iroh and applied through cr-sqlite, with +schema hash gating and resource authorization. + +## Consequences + +The MVP stores DB resource metadata and leaves synchronization as explicit future +work. diff --git a/docs/adr/0011-kv-iroh-documents.md b/docs/adr/0011-kv-iroh-documents.md new file mode 100644 index 0000000..fd440ab --- /dev/null +++ b/docs/adr/0011-kv-iroh-documents.md @@ -0,0 +1,15 @@ +# ADR 0011: KV With Iroh Documents + +## Status + +Accepted. + +## Decision + +KV resources will use Iroh Documents namespaces for small replicated state, +configuration, metadata, preferences, and script state. Authorization can include +prefix-scoped capabilities. + +## Consequences + +The MVP exposes CLI shape and types while deferring iroh-docs API pinning. diff --git a/docs/adr/0012-automerge-documents.md b/docs/adr/0012-automerge-documents.md new file mode 100644 index 0000000..ec6f924 --- /dev/null +++ b/docs/adr/0012-automerge-documents.md @@ -0,0 +1,16 @@ +# ADR 0012: Automerge Documents + +## Status + +Accepted. + +## Decision + +Document resources will use Automerge for JSON-like CRDT state synchronized over +Iroh streams. Documents may carry or delegate to their own resource-local auth +state. + +## Consequences + +This leaves a path to multi-user local-first documents without implementing the +full cryptographic model during bootstrap. diff --git a/docs/adr/0013-pipe-and-pubsub.md b/docs/adr/0013-pipe-and-pubsub.md new file mode 100644 index 0000000..e8c97e0 --- /dev/null +++ b/docs/adr/0013-pipe-and-pubsub.md @@ -0,0 +1,19 @@ +# ADR 0013: Pipe And Pubsub + +## Status + +Accepted. + +## Decision + +Pipe resources will use authorized Iroh streams for stdin/stdout-style byte +streams and forwarding. Pubsub will use lossy gossip for wakeups, presence, +status, and ephemeral notifications. + +Pubsub is not authoritative storage. Important facts must live in durable +resources such as kv, document, db, CAS manifests, or future auth logs. + +## Consequences + +The modules can support ad hoc workflow and presence without confusing gossip +with durable state. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..1cd32c2 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,88 @@ +# Architecture + +`geth` is a single-binary local-first mesh runtime. One executable provides both +daemon mode and control mode. The daemon owns local identity, metadata storage, +the future shared Iroh endpoint, resource registry, module routing, and local +control socket. Control commands connect to the Unix socket and send typed JSONL +requests. + +## Iroh-Only Remote Communication + +Remote geth node-to-node communication is Iroh-only. The daemon will own one +shared Iroh endpoint and register module protocols on ALPNs such as +`/geth/cas/1`, `/geth/kv/1`, `/geth/pipe/1`, and `/geth/ssh-proxy/1`. + +SSH keys are not transport keys. They are admin trust anchors and signing +identities for keychain and authorization operations. SSH proxying, when added, +will carry SSH bytes over an authorized Iroh stream and will not make SSH a geth +transport backend. + +## Resource Model + +Everything meaningful is modeled as a resource. Resources have a kind, name, +authority reference, local role, replication policy, retention policy, and +status. Authorization is resource-scoped and capability-based. + +Resource kinds: + +- `db`: cr-sqlite-backed SQLite synchronization +- `kv`: Iroh Documents backed key-value data +- `pipe`: authorized byte streams and forwarding +- `document`: Automerge CRDT documents +- `pubsub`: lossy notifications and presence +- `cas`: content-addressed blobs +- `ssh-proxy`: SSH/admin proxying over Iroh + +## Module Overview + +`geth-cas` is implemented locally first using BLAKE3 hashes and filesystem blob +storage. Iroh-blobs, providers, encrypted blobs, manifests, and file sync trees +are future work. + +`geth-kv`, `geth-db`, `geth-document`, `geth-pubsub`, `geth-pipe`, and +`geth-ssh-proxy` currently define types, command shape, and roadmap stubs. + +## Keychain, Auth, And Secrets + +The identity plane is `geth-keychain`: admin keys, users, devices, nodes, agents, +and endpoint bindings. Endpoint rotation must not destroy higher-level node +identity. + +The authorization plane is `geth-auth`: resource-local signed operation logs, +grants, revocations, groups, and `auth explain`. + +The payload access plane is `geth-secrets`: resource master secrets, epochs, +key envelopes, bearer secrets, and rotation. Revocation for private data is +modeled initially as secret epoch rotation. + +## Multi-User Direction + +The project is structured for future multi-user local-first authorization: + +- authorization is replicated data, not one mutable ACL blob +- resources can carry or delegate to their own auth state +- users, devices, nodes, agents, and endpoints are separate principals +- capabilities are the underlying permission unit +- bearer access is resource-scoped and does not mutate the trust graph +- offline revocation is eventual +- encryption key distribution is part of authorization + +## Keyhive/BeeKEM Roadmap + +Resource secret epochs are the v0/v1 approximation for private payload access. +Later designs can add Keyhive-like convergent capabilities and BeeKEM/CGKA-style +group key evolution. The bootstrap does not implement BeeKEM and does not claim +strong forward secrecy or post-compromise security. + +## Security Invariants + +- All remote node-to-node communication is over Iroh. +- SSH is not a geth transport. +- SSH keys are admin trust anchors and signing identities. +- Agent/node keys handle routine local identity. +- Discovery is untrusted. +- Knowing an EndpointID does not grant access. +- Bearer secrets are resource-scoped capabilities. +- Bearer access does not imply trust graph mutation rights. +- Authorization is capability-based and resource-scoped. +- Network and control decoders treat input as untrusted. diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..934aaee --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,57 @@ +# Roadmap + +## Phase 0: Bootstrap + +- single CLI +- local daemon +- local store +- local identity +- local CAS +- docs and ADRs + +## Phase 1: Iroh Foundation + +- Iroh endpoint +- peer cards +- peer discovery +- peer exchange +- basic authenticated peer connection + +## Phase 2: Trust And Authorization + +- SSH-admin-rooted keychain +- signed keychain ops +- resource auth ops +- grants and revocations +- auth explain +- resource secrets +- bearer invites + +## Phase 3: CAS/KV/Pubsub + +- iroh-blobs CAS +- iroh-docs KV +- iroh-gossip pubsub +- private topics and encrypted values + +## Phase 4: Pipes And SSH Proxy + +- dumbpipe-style streams +- TCP and Unix socket forwarding +- SSH proxy over Iroh +- restricted geth admin shell + +## Phase 5: DB And Documents + +- cr-sqlite sync +- Automerge sync +- resource-attached authorization +- secret-derived encryption + +## Phase 6: File Sync And Advanced Local-First Auth + +- CAS tree objects +- file roots +- conflict handling +- Keyhive-like convergent capabilities +- BeeKEM/CGKA-inspired group key evolution diff --git a/justfile b/justfile new file mode 100644 index 0000000..c648ebe --- /dev/null +++ b/justfile @@ -0,0 +1,17 @@ +fmt: + cargo fmt --all + +check: + cargo check --workspace --all-targets + +test: + cargo test --workspace + +clippy: + cargo clippy --workspace --all-targets -- -D warnings + +ci: + cargo fmt --all -- --check + cargo check --workspace --all-targets + cargo clippy --workspace --all-targets -- -D warnings + cargo test --workspace diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..ed49ca7 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +edition = "2024" +max_width = 100