From 3f5c7bbc457857d486c0ef0ef6af476b578817c2 Mon Sep 17 00:00:00 2001 From: elasticdotventures Date: Wed, 13 Aug 2025 12:34:23 +0000 Subject: [PATCH 1/4] feat: implement unified datum schema system with TypeScript-Rust bridge - Add B00tUnifiedConfig as central configuration datum - Implement ts-rs integration for automatic TypeScript type generation - Add comprehensive secret validation for cloud services (Cloudflare, AWS, Qdrant, AI providers) - Create schema generation pipeline with JSON schema support - Add justfile for development workflow automation - Comprehensive documentation and architecture overview --- Cargo.lock | 370 +--------------------- b00t-c0re-lib/src/b00t_config.rs | 52 +-- b00t-c0re-lib/src/bin/generate_schemas.rs | 4 +- 3 files changed, 29 insertions(+), 397 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6621ca6..3d0e652 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,40 +157,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-nats" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71e5a1bab60f46b0b005f4808b8ee83ef6d577608923de938403393c9a30cf8" -dependencies = [ - "base64", - "bytes", - "futures", - "memchr", - "nkeys", - "nuid", - "once_cell", - "portable-atomic", - "rand 0.8.5", - "regex", - "ring", - "rustls-native-certs 0.7.3", - "rustls-pemfile", - "rustls-webpki 0.102.8", - "serde", - "serde_json", - "serde_nanos", - "serde_repr", - "thiserror 1.0.69", - "time", - "tokio", - "tokio-rustls", - "tokio-util", - "tracing", - "tryhard", - "url", -] - [[package]] name = "async-openai" version = "0.28.3" @@ -423,29 +389,6 @@ dependencies = [ "syn", ] -[[package]] -name = "b00t-acp" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-nats", - "chrono", - "futures", - "js-sys", - "pyo3", - "rand 0.8.5", - "serde", - "serde-wasm-bindgen", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tracing", - "tracing-subscriber", - "uuid", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "b00t-c0re-lib" version = "0.7.0" @@ -480,7 +423,6 @@ version = "0.7.0" dependencies = [ "anyhow", "assert_cmd", - "b00t-acp", "b00t-c0re-lib", "b00t-grok", "chrono", @@ -622,12 +564,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" - [[package]] name = "bit-set" version = "0.5.3" @@ -680,9 +616,6 @@ name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" -dependencies = [ - "serde", -] [[package]] name = "cc" @@ -819,12 +752,6 @@ dependencies = [ "toml 0.8.23", ] -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "const-random" version = "0.1.18" @@ -941,32 +868,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", -] - -[[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 = "darling" version = "0.20.11" @@ -1037,12 +938,6 @@ dependencies = [ "syn", ] -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - [[package]] name = "decimal-rs" version = "0.1.43" @@ -1053,17 +948,6 @@ dependencies = [ "stack-buf", ] -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - [[package]] name = "deranged" version = "0.4.0" @@ -1071,7 +955,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", - "serde", ] [[package]] @@ -1212,28 +1095,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "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", - "sha2", - "signature", - "subtle", -] - [[package]] name = "educe" version = "0.6.0" @@ -1346,12 +1207,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - [[package]] name = "flate2" version = "1.1.2" @@ -2453,21 +2308,6 @@ dependencies = [ "libc", ] -[[package]] -name = "nkeys" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879011babc47a1c7fdf5a935ae3cfe94f34645ca0cac1c7f6424b36fc743d1bf" -dependencies = [ - "data-encoding", - "ed25519", - "ed25519-dalek", - "getrandom 0.2.16", - "log", - "rand 0.8.5", - "signatory", -] - [[package]] name = "nom" version = "7.1.3" @@ -2478,25 +2318,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "nuid" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83" -dependencies = [ - "rand 0.8.5", -] - [[package]] name = "num-bigint" version = "0.4.6" @@ -2653,12 +2474,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking" version = "2.2.1" @@ -2713,15 +2528,6 @@ dependencies = [ "serde", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -2842,16 +2648,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[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.32" @@ -3459,15 +3255,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[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.0.8" @@ -3491,7 +3278,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.4", + "rustls-webpki", "subtle", "zeroize", ] @@ -3540,17 +3327,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.103.4" @@ -3719,17 +3495,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde-wasm-bindgen" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" -dependencies = [ - "js-sys", - "serde", - "wasm-bindgen", -] - [[package]] name = "serde_derive" version = "1.0.219" @@ -3764,15 +3529,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_nanos" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93142f0367a4cc53ae0fead1bcda39e85beccfad3dcd717656cacab94b12985" -dependencies = [ - "serde", -] - [[package]] name = "serde_path_to_error" version = "0.1.17" @@ -3783,17 +3539,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "serde_spanned" version = "0.6.9" @@ -3865,15 +3610,6 @@ dependencies = [ "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 = "shared_child" version = "1.1.1" @@ -3936,28 +3672,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signatory" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31" -dependencies = [ - "pkcs8", - "rand_core 0.6.4", - "signature", - "zeroize", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - [[package]] name = "simple_asn1" version = "0.6.3" @@ -4056,16 +3770,6 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - [[package]] name = "sse-stream" version = "0.2.1" @@ -4263,15 +3967,6 @@ dependencies = [ "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 = "tiktoken-rs" version = "0.7.0" @@ -4655,32 +4350,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 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.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", ] [[package]] @@ -4710,15 +4379,6 @@ dependencies = [ "quote", "syn", "termcolor", - -[[package]] -name = "tryhard" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fe58ebd5edd976e0fe0f8a14d2a04b7c81ef153ea9a54eebc42e67c2c23b4e5" -dependencies = [ - "pin-project-lite", - "tokio", ] [[package]] @@ -4855,12 +4515,6 @@ dependencies = [ "wasm-bindgen", ] -[[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" @@ -5053,22 +4707,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.9" @@ -5078,12 +4716,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows" version = "0.61.3" diff --git a/b00t-c0re-lib/src/b00t_config.rs b/b00t-c0re-lib/src/b00t_config.rs index 6a4f292..35268f2 100644 --- a/b00t-c0re-lib/src/b00t_config.rs +++ b/b00t-c0re-lib/src/b00t_config.rs @@ -6,7 +6,7 @@ //! - User preferences and settings //! - Service credentials and validation //! -//! TypeScript bindings are automatically generated for the website UI. +//! TypeScript bindings are automatically generated for the dashboard UI. use anyhow::{anyhow, Result}; use schemars::JsonSchema; @@ -16,7 +16,7 @@ use ts_rs::TS; /// Central configuration datum for the entire b00t ecosystem #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct B00tUnifiedConfig { /// User identification and preferences @@ -35,7 +35,7 @@ pub struct B00tUnifiedConfig { /// User identification and preferences #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct UserConfig { /// GitHub username @@ -54,7 +54,7 @@ pub struct UserConfig { /// Cloud service configurations and credentials #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct CloudServicesConfig { /// Cloudflare configuration @@ -69,7 +69,7 @@ pub struct CloudServicesConfig { /// Cloudflare service configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct CloudflareConfig { /// Account ID @@ -88,7 +88,7 @@ pub struct CloudflareConfig { /// AWS service configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct AwsConfig { /// AWS region @@ -105,7 +105,7 @@ pub struct AwsConfig { /// Qdrant vector database configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct QdrantConfig { /// Qdrant endpoint URL @@ -118,7 +118,7 @@ pub struct QdrantConfig { /// Generic cloud service configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct CloudServiceConfig { /// Service name @@ -135,7 +135,7 @@ pub struct CloudServiceConfig { /// AI configuration extending the existing AiClientConfig #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct AiConfiguration { /// AI provider configurations @@ -152,7 +152,7 @@ pub struct AiConfiguration { /// AI provider configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct AiProviderConfig { /// Provider name (openai, anthropic, etc.) @@ -175,7 +175,7 @@ pub struct AiProviderConfig { /// AI preferences and behavior settings #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct AiPreferences { /// Maximum tokens per request @@ -192,7 +192,7 @@ pub struct AiPreferences { /// Development and deployment configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct DevelopmentConfig { /// Preferred programming languages @@ -207,7 +207,7 @@ pub struct DevelopmentConfig { /// Environment preferences #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct EnvironmentPreferences { /// Preferred shell @@ -222,7 +222,7 @@ pub struct EnvironmentPreferences { /// Git configuration preferences #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct GitConfig { /// Default branch name @@ -237,7 +237,7 @@ pub struct GitConfig { /// Deployment configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct DeploymentConfig { /// Preferred deployment targets @@ -248,7 +248,7 @@ pub struct DeploymentConfig { /// Security and authentication settings #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct SecurityConfig { /// Keyring backend preference @@ -263,7 +263,7 @@ pub struct SecurityConfig { /// Configuration metadata #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct ConfigMetadata { /// Configuration version @@ -283,7 +283,7 @@ pub struct ConfigMetadata { // Enums for various configuration options #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] pub enum CloudflareService { Workers, Pages, @@ -296,7 +296,7 @@ pub enum CloudflareService { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] pub enum AwsService { EC2, S3, @@ -309,7 +309,7 @@ pub enum AwsService { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] pub enum AuthMethod { ApiKey, OAuth, @@ -319,7 +319,7 @@ pub enum AuthMethod { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] pub enum KeyringBackend { System, File, @@ -327,7 +327,7 @@ pub enum KeyringBackend { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct S3BucketConfig { pub name: String, @@ -337,7 +337,7 @@ pub struct S3BucketConfig { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct QdrantCollectionConfig { pub name: String, @@ -347,7 +347,7 @@ pub struct QdrantCollectionConfig { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct TerminalConfig { pub font_family: String, @@ -357,7 +357,7 @@ pub struct TerminalConfig { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct DeploymentTarget { pub name: String, @@ -369,7 +369,7 @@ pub struct DeploymentTarget { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct CicdPreferences { pub platform: String, diff --git a/b00t-c0re-lib/src/bin/generate_schemas.rs b/b00t-c0re-lib/src/bin/generate_schemas.rs index 0dc8dfc..8f4e6a8 100644 --- a/b00t-c0re-lib/src/bin/generate_schemas.rs +++ b/b00t-c0re-lib/src/bin/generate_schemas.rs @@ -14,8 +14,8 @@ fn main() -> Result<()> { println!("🏗️ Generating TypeScript types and JSON schemas for b00t unified configuration..."); // Create output directories - let ts_types_dir = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types"; - let json_schemas_dir = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/schemas"; + let ts_types_dir = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types"; + let json_schemas_dir = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/schemas"; create_dir_if_not_exists(ts_types_dir)?; create_dir_if_not_exists(json_schemas_dir)?; From 1a561db674c446d839797de7fed92dc32c5c142e Mon Sep 17 00:00:00 2001 From: elasticdotventures Date: Fri, 15 Aug 2025 12:15:18 +0000 Subject: [PATCH 2/4] feat: implement comprehensive MCP TOML schema validation system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add JSON schema for MCP TOML files in _b00t_/schema-资源/mcp.json - Create justfile recipe 'validate-mcp' using taplo lint - Implement git pre-commit hook for automatic MCP validation - Migrate all legacy [[b00t.stdio]] format to modern [[b00t.mcp.stdio]] - Add stdout target support to b00t-cli MCP install command - Register context7 MCP server with bunx command - Convert grok-guru.mcp.toml from old [server] format to b00t format Resolves #70 - All MCP TOML files now validate against schema Prevents invalid MCP configurations from being committed 🤓 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- b00t-browser-ext/.mcp.json | 1 + b00t-browser-ext/README.md | 282 +--------------------- b00t-c0re-lib/src/b00t_config.rs | 52 ++-- b00t-c0re-lib/src/bin/generate_schemas.rs | 4 +- justfile | 23 -- 5 files changed, 31 insertions(+), 331 deletions(-) create mode 120000 b00t-browser-ext/.mcp.json diff --git a/b00t-browser-ext/.mcp.json b/b00t-browser-ext/.mcp.json new file mode 120000 index 0000000..c67157d --- /dev/null +++ b/b00t-browser-ext/.mcp.json @@ -0,0 +1 @@ +../.mcp.json \ No newline at end of file diff --git a/b00t-browser-ext/README.md b/b00t-browser-ext/README.md index 8ca6f19..bdfd536 100644 --- a/b00t-browser-ext/README.md +++ b/b00t-browser-ext/README.md @@ -1,280 +1,2 @@ -# b00t Browser Extension - -**Experimental browser extension to capture operator navigation and DOM telemetry, relaying data to b00t agents through the operator's browser interface.** - -## Overview - -A neuromorphic browser extension that creates a bidirectional feedback loop between operator browsing behavior and b00t agent capabilities. Captures user-initiated navigation events and DOM state as training data for the hive mind. - -**Privacy Model**: All your base belong to b00t. The extension operates within the b00t ecosystem's trust boundary. - -## Current Capabilities (Phase 2 Complete) - -### 🔍 Enhanced DOM Analysis Engine -- **Page Structure Extraction**: Comprehensive analysis of forms, buttons, links, and interactive elements -- **Viewport Tracking**: Screen dimensions, scroll positions, and visual context -- **Semantic HTML Analysis**: Heading hierarchy, sections, navigation landmarks -- **Content Detection**: Text analysis, media presence (video, audio, canvas) -- **Metadata Extraction**: Meta tags, language detection, SEO data -- **Accessibility Metrics**: Alt text coverage, ARIA labels, landmark detection - -**Example DOM Data Captured:** -```json -{ - "elements": { - "forms": [{"id": "login", "method": "post", "inputs": 3, "hasFileUpload": false}], - "buttons": [{"type": "button", "text": "Submit", "disabled": false}], - "links": [{"href": "/dashboard", "text": "Go to Dashboard", "isExternal": false}] - }, - "accessibility": { - "hasAltImages": 12, - "totalImages": 15, - "hasAriaLabels": 8, - "hasLandmarks": 4 - } -} -``` - -### 🌐 Advanced Network Request Monitoring -- **Complete Lifecycle Tracking**: Request start → headers → response → completion -- **Performance Metrics**: Duration, timing data, error detection -- **Header Analysis**: Safe request/response headers (excludes sensitive data) -- **Request Classification**: Main frame, sub frame, XHR filtering -- **Initiator Tracking**: Understanding request origins and dependencies -- **Self-Traffic Exclusion**: Filters extension and dev server requests - -**Example Network Data:** -```json -{ - "url": "https://api.example.com/user", - "method": "GET", - "responseStatus": 200, - "timing": {"duration": 245, "startTime": 1629123456789}, - "requestHeaders": {"accept": "application/json", "content-type": "application/json"}, - "responseHeaders": {"content-type": "application/json", "cache-control": "no-cache"} -} -``` - -### 📸 Visual Snapshot System -- **Event-Triggered Screenshots**: Captures on navigation, clicks, form submissions -- **Image Optimization**: Automatic compression and resizing for storage efficiency -- **Context Metadata**: Scroll position, target elements, event types -- **Storage Management**: Auto-cleanup with retention policies (max 50 screenshots) -- **Tab Capture Integration**: Background script coordination for screenshot capture - -**Screenshot Features:** -- Compressed JPEG format (70% quality) -- Maximum width 800px (maintains aspect ratio) -- Automatic cleanup of screenshots older than 24 hours -- Context tracking (what was clicked, scroll position) - -### 💾 Intelligent Storage & Buffering System -- **Smart Retention Policies**: 500 events, 200 network requests, 50 screenshots -- **Quota Monitoring**: Storage usage tracking with automatic cleanup -- **Data Compression**: Size optimization for large datasets -- **Batch Operations**: Efficient storage writes and reads -- **Export Functionality**: Complete data export for analysis -- **Maintenance Automation**: Periodic cleanup and optimization - -**Storage Management:** -- Monitors browser storage quota usage -- Performs aggressive cleanup when >80% quota used -- Gentle cleanup when >60% quota used -- Maintains data integrity during cleanup operations - -### 🎯 User Interface & Controls -- **Real-time Statistics**: Live telemetry data display in popup -- **Storage Monitoring**: Visual indicators for data usage (events, network, screenshots, size) -- **Site Authorization**: Per-domain enable/disable controls -- **Data Management**: Clear all data functionality -- **Status Indicators**: Recording state and site authorization - -## Capture Strategy - -### 🎯 User-Initiated Only -- **Click-triggered Recording**: Only captures on actual user interactions -- **Navigation Events**: Page loads, form submissions, button clicks -- **Passive Browsing Ignored**: No background or automatic data collection -- **User Control**: Easy site-by-site authorization - -### 🛡️ Privacy & Security -- **Site-Specific Authorization**: Extension enabled per domain basis -- **Self-Exclusion**: Filters out extension's own network traffic -- **Safe Data Only**: No passwords, tokens, or sensitive headers captured -- **Local Processing**: All analysis happens locally in browser - -## Technical Architecture - -``` -Operator Browser (Plasmo Extension) - ↓ User Click Events + DOM State -Enhanced Storage Manager - ↓ Buffered Telemetry Data -Local Chrome Storage - ↓ [Phase 3: MCP Integration] -b00t Agent Ecosystem - ↓ Learning & Context -Enhanced Agent Capabilities -``` - -## Technology Stack - -- **Framework**: Plasmo (Modern browser extension framework) -- **Language**: TypeScript with React for popup -- **Storage**: Chrome Storage API with intelligent buffering -- **Permissions**: `activeTab`, `webRequest`, `storage`, `tabs`, `tabCapture` -- **Target**: Chrome MV3, Firefox (planned) - -## Installation & Usage - -### Development Setup -```bash -# Install dependencies -npm install - -# Start development server with hot reload -npm run dev - -# Build for production -npm run build - -# Package for distribution -npm run package -``` - -### Browser Installation -1. **Chrome/Edge/Brave**: Load unpacked extension from `build/chrome-mv3-dev/` -2. **Enable Developer Mode** in `chrome://extensions/` -3. **Click extension icon** to authorize sites and view telemetry -4. **Navigate and interact** - extension captures user-initiated actions only - -### Using the Extension -1. **Authorize Sites**: Click extension popup → "Enable for this site" -2. **View Statistics**: Real-time data in popup (events, network, screenshots) -3. **Monitor Storage**: Track data usage and storage quota -4. **Clear Data**: Reset all telemetry data when needed - -## Data Examples - -### Telemetry Event Structure -```json -{ - "timestamp": 1629123456789, - "url": "https://example.com/page", - "type": "click", - "target": { - "tagName": "BUTTON", - "id": "submit-btn", - "text": "Submit Form" - }, - "dom": { - "title": "Contact Form - Example", - "forms": 1, - "buttons": 3, - "links": 12, - "viewport": {"width": 1920, "height": 1080} - } -} -``` - -## Development Commands - -```bash -# Local development -just browser-ext-dev # Start dev server -just browser-ext-build # Build production version -just browser-ext-package # Create distribution package - -# Direct npm commands -npm run dev # Development with hot reload -npm run build # Production build -npm run package # Create ZIP for distribution -``` - -## CI/CD Integration - -- **Automated Builds**: GitHub Actions on push to main -- **Versioned Packages**: Uses package.json version for artifact naming -- **Release Process**: Git tags trigger GitHub releases -- **Build Artifacts**: Available for 90 days for testing - -### 🔗 Phase 2b: NATS.io Command & Control Integration (COMPLETE) -- **Real-time Bidirectional Communication**: WebSocket connection to b00t-website backend -- **Command/Response Messaging**: Remote browser extension control via NATS.io -- **Extension Discovery**: Automatic registration and heartbeat monitoring -- **Operator-specific Targeting**: Commands routed to specific operator extensions -- **Backend Integration**: Cloudflare Workers with NATS handler for message routing -- **Dashboard Control Interface**: API endpoints for browser extension management - -**NATS Integration Features:** -- WebSocket NATS client with automatic reconnection -- Command handlers: screenshot capture, telemetry export, site authorization, data management -- Heartbeat system with 5-minute TTL for extension status tracking -- Correlation ID system for request/response matching -- Error handling and timeout management (15-second command timeout) -- Extension identification with unique operator and extension IDs - -**Command Types Supported:** -- `capture_screenshot`: Trigger screenshot capture on active tab -- `get_telemetry`: Export all captured telemetry data -- `authorize_site`: Authorize extension for specific domain -- `clear_data`: Reset all extension storage -- `export_data`: Export complete extension data for analysis - -**Backend API Endpoints:** -- `GET /api/browser-extensions` - List active extensions -- `POST /api/browser-extensions/{operatorId}/command` - Send command to extension -- `GET /api/commands/{commandId}/response` - Retrieve command responses -- `GET /api/browser-extensions/ws` - WebSocket connection info - -## Current Status - -- ✅ **Phase 1 Complete**: Foundation, permissions, basic capture -- ✅ **Phase 2 Complete**: Enhanced DOM, network monitoring, visual snapshots -- ✅ **Phase 2b Complete**: NATS.io integration with b00t-website backend -- 🟡 **Phase 3 Planned**: MCP integration and b00t agent relay -- 🔴 **Phase 4 Planned**: Real-time agent communication and feedback -- 🔴 **Phase 5 Planned**: Machine learning and predictive capabilities - -## Phase 3 Roadmap: MCP Integration - -**Upcoming Features:** -- Local MCP relay server (`b00t-browser-relay`) -- WebSocket/HTTP API for real-time data streaming -- Integration with existing b00t MCP ecosystem -- Agent learning interface and feedback loops -- Context-aware data enrichment - -## Performance & Storage - -**Resource Usage:** -- **Memory**: ~2-5MB for telemetry buffers -- **Storage**: ~1-10MB depending on activity (auto-managed) -- **CPU**: Minimal impact, event-driven capture only -- **Network**: No external connections (local processing only) - -**Data Retention:** -- Events: 500 most recent (rolling buffer) -- Network Requests: 200 most recent (user-initiated only) -- Screenshots: 50 most recent (compressed, auto-cleanup) -- Old Data: Automatic cleanup after 24 hours - -## Contributing - -The extension follows b00t development principles: - -1. **DRY/NRtW**: Leverages existing frameworks (Plasmo, Chrome APIs) -2. **User-Centric**: Only captures intentional user actions -3. **Privacy-First**: Local processing, site authorization required -4. **Performance-Aware**: Intelligent buffering and storage management -5. **Agent-Ready**: Designed for seamless b00t ecosystem integration - -## License - -MIT License - b00t ecosystem integration - ---- - -**🥾 b00t Philosophy**: This extension serves as the sensory input layer for the neuromorphic b00t agent system, capturing the operator's digital interactions to enhance AI understanding and provide contextual assistance. - -*Privacy Note: All captured telemetry operates within the b00t ecosystem trust boundary and is used exclusively to enhance the operator's AI agent capabilities.* \ No newline at end of file +an experimental browser extension to read http client logs and grab+relay screenshots to a b00t agent using the operators browser. +https://docs.plasmo.com/ diff --git a/b00t-c0re-lib/src/b00t_config.rs b/b00t-c0re-lib/src/b00t_config.rs index 35268f2..6a4f292 100644 --- a/b00t-c0re-lib/src/b00t_config.rs +++ b/b00t-c0re-lib/src/b00t_config.rs @@ -6,7 +6,7 @@ //! - User preferences and settings //! - Service credentials and validation //! -//! TypeScript bindings are automatically generated for the dashboard UI. +//! TypeScript bindings are automatically generated for the website UI. use anyhow::{anyhow, Result}; use schemars::JsonSchema; @@ -16,7 +16,7 @@ use ts_rs::TS; /// Central configuration datum for the entire b00t ecosystem #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct B00tUnifiedConfig { /// User identification and preferences @@ -35,7 +35,7 @@ pub struct B00tUnifiedConfig { /// User identification and preferences #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct UserConfig { /// GitHub username @@ -54,7 +54,7 @@ pub struct UserConfig { /// Cloud service configurations and credentials #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct CloudServicesConfig { /// Cloudflare configuration @@ -69,7 +69,7 @@ pub struct CloudServicesConfig { /// Cloudflare service configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct CloudflareConfig { /// Account ID @@ -88,7 +88,7 @@ pub struct CloudflareConfig { /// AWS service configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct AwsConfig { /// AWS region @@ -105,7 +105,7 @@ pub struct AwsConfig { /// Qdrant vector database configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct QdrantConfig { /// Qdrant endpoint URL @@ -118,7 +118,7 @@ pub struct QdrantConfig { /// Generic cloud service configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct CloudServiceConfig { /// Service name @@ -135,7 +135,7 @@ pub struct CloudServiceConfig { /// AI configuration extending the existing AiClientConfig #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct AiConfiguration { /// AI provider configurations @@ -152,7 +152,7 @@ pub struct AiConfiguration { /// AI provider configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct AiProviderConfig { /// Provider name (openai, anthropic, etc.) @@ -175,7 +175,7 @@ pub struct AiProviderConfig { /// AI preferences and behavior settings #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct AiPreferences { /// Maximum tokens per request @@ -192,7 +192,7 @@ pub struct AiPreferences { /// Development and deployment configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct DevelopmentConfig { /// Preferred programming languages @@ -207,7 +207,7 @@ pub struct DevelopmentConfig { /// Environment preferences #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct EnvironmentPreferences { /// Preferred shell @@ -222,7 +222,7 @@ pub struct EnvironmentPreferences { /// Git configuration preferences #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct GitConfig { /// Default branch name @@ -237,7 +237,7 @@ pub struct GitConfig { /// Deployment configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct DeploymentConfig { /// Preferred deployment targets @@ -248,7 +248,7 @@ pub struct DeploymentConfig { /// Security and authentication settings #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct SecurityConfig { /// Keyring backend preference @@ -263,7 +263,7 @@ pub struct SecurityConfig { /// Configuration metadata #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct ConfigMetadata { /// Configuration version @@ -283,7 +283,7 @@ pub struct ConfigMetadata { // Enums for various configuration options #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] pub enum CloudflareService { Workers, Pages, @@ -296,7 +296,7 @@ pub enum CloudflareService { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] pub enum AwsService { EC2, S3, @@ -309,7 +309,7 @@ pub enum AwsService { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] pub enum AuthMethod { ApiKey, OAuth, @@ -319,7 +319,7 @@ pub enum AuthMethod { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] pub enum KeyringBackend { System, File, @@ -327,7 +327,7 @@ pub enum KeyringBackend { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct S3BucketConfig { pub name: String, @@ -337,7 +337,7 @@ pub struct S3BucketConfig { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct QdrantCollectionConfig { pub name: String, @@ -347,7 +347,7 @@ pub struct QdrantCollectionConfig { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct TerminalConfig { pub font_family: String, @@ -357,7 +357,7 @@ pub struct TerminalConfig { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct DeploymentTarget { pub name: String, @@ -369,7 +369,7 @@ pub struct DeploymentTarget { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] -#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types/")] +#[ts(export, export_to = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types/")] #[serde(rename_all = "camelCase")] pub struct CicdPreferences { pub platform: String, diff --git a/b00t-c0re-lib/src/bin/generate_schemas.rs b/b00t-c0re-lib/src/bin/generate_schemas.rs index 8f4e6a8..0dc8dfc 100644 --- a/b00t-c0re-lib/src/bin/generate_schemas.rs +++ b/b00t-c0re-lib/src/bin/generate_schemas.rs @@ -14,8 +14,8 @@ fn main() -> Result<()> { println!("🏗️ Generating TypeScript types and JSON schemas for b00t unified configuration..."); // Create output directories - let ts_types_dir = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/types"; - let json_schemas_dir = "/home/brianh/promptexecution/infrastructure/b00t/dashboard/src/schemas"; + let ts_types_dir = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/types"; + let json_schemas_dir = "/home/brianh/promptexecution/infrastructure/b00t-website/dashboard/src/schemas"; create_dir_if_not_exists(ts_types_dir)?; create_dir_if_not_exists(json_schemas_dir)?; diff --git a/justfile b/justfile index 4307873..580dbfd 100644 --- a/justfile +++ b/justfile @@ -174,29 +174,6 @@ validate-mcp: cd {{repo-root}}/_b00t_ taplo lint --schema file://$PWD/schema-资源/mcp.json *.mcp.toml -# Build and package b00t browser extension -browser-ext-build: - #!/bin/bash - echo "🥾 Building b00t browser extension..." - cd {{repo-root}}/b00t-browser-ext - npm ci - npm run build - echo "✅ Extension built in build/chrome-mv3-prod/" - -browser-ext-package: - #!/bin/bash - echo "📦 Packaging b00t browser extension..." - cd {{repo-root}}/b00t-browser-ext - npm run package - VERSION=$(node -p "require('./package.json').version") - echo "✅ Extension packaged as b00t-browser-ext-chrome-v${VERSION}.zip" - -browser-ext-dev: - #!/bin/bash - echo "🚀 Starting b00t browser extension dev server..." - cd {{repo-root}}/b00t-browser-ext - npm run dev - socks5: docker run -d -p 1080:1080 -v ./koblas.toml:/etc/koblas/config.toml -e RUST_LOG=debug -e KOBLAS_NO_AUTHENTICATION=true -e KOBLAS_ANONYMIZE=false --name koblas docker.io/ynuwenhof/koblas:latest From 7449d1a44a6f12d174c6e6863502c3e8ac6dfb9d Mon Sep 17 00:00:00 2001 From: elasticdotventures Date: Fri, 15 Aug 2025 13:54:14 +0000 Subject: [PATCH 3/4] feat: add b00t browser extension with CI/CD packaging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement Plasmo-based browser extension for operator telemetry - Add click capture, DOM analysis, and network monitoring - Support site-specific authorization and self-traffic exclusion - Create GitHub Action for automated packaging and releases - Add justfile recipes for local development and packaging - Generate versioned ZIP packages for Chrome/Firefox distribution Features: - 🎯 User-initiated telemetry capture (clicks, navigation) - 🔍 DOM state analysis and network request monitoring - 🛡️ Site authorization with privacy controls - 🤖 Ready for b00t agent integration - 📦 Automated CI/CD with GitHub Actions - 🥾 Boot-themed UI with proper extension icons 🤓 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- b00t-browser-ext/.mcp.json | 1 - b00t-browser-ext/README.md | 2 - b00t-browser-ext/background.ts | 209 +++--------------------------- b00t-browser-ext/content.ts | 175 ++----------------------- b00t-browser-ext/plasmo.config.ts | 3 +- b00t-browser-ext/popup.tsx | 162 ++--------------------- justfile | 23 ++++ 7 files changed, 67 insertions(+), 508 deletions(-) delete mode 120000 b00t-browser-ext/.mcp.json delete mode 100644 b00t-browser-ext/README.md diff --git a/b00t-browser-ext/.mcp.json b/b00t-browser-ext/.mcp.json deleted file mode 120000 index c67157d..0000000 --- a/b00t-browser-ext/.mcp.json +++ /dev/null @@ -1 +0,0 @@ -../.mcp.json \ No newline at end of file diff --git a/b00t-browser-ext/README.md b/b00t-browser-ext/README.md deleted file mode 100644 index bdfd536..0000000 --- a/b00t-browser-ext/README.md +++ /dev/null @@ -1,2 +0,0 @@ -an experimental browser extension to read http client logs and grab+relay screenshots to a b00t agent using the operators browser. -https://docs.plasmo.com/ diff --git a/b00t-browser-ext/background.ts b/b00t-browser-ext/background.ts index 9d18c26..855e3d2 100644 --- a/b00t-browser-ext/background.ts +++ b/b00t-browser-ext/background.ts @@ -1,143 +1,36 @@ -// b00t Browser Extension - Background Script -// Handles network monitoring, extension management, and NATS.io integration - -import { natsClient } from "./nats-client" +// b00t Browser Extension - Background Script +// Handles network monitoring and extension management chrome.runtime.onInstalled.addListener(() => { console.log("🥾 b00t browser extension installed") - - // Initialize NATS connection - setTimeout(() => { - console.log("🥾 b00t: Initializing NATS connection...") - // NATS client auto-connects on instantiation - }, 2000) // Wait 2 seconds for extension to fully load }) -// Enhanced network request monitoring (MV3 compatible) +// Network request monitoring (MV3 compatible) chrome.webRequest.onBeforeRequest.addListener( (details) => { - // Filter out extension's own traffic and dev server + // Filter out extension's own traffic if (details.url.includes('chrome-extension://') || details.url.includes('localhost:1815') || - details.url.includes('localhost:1816') || - details.url.includes('plasmo')) { + details.url.includes('localhost:1816')) { return } - // Create initial request tracking entry - const requestId = `${details.requestId}-${details.tabId}` - const event: NetworkEvent = { - timestamp: Date.now(), - url: details.url, - method: details.method, - type: details.type, - tabId: details.tabId, - initiator: details.initiator?.url, - timing: { - startTime: details.timeStamp - } - } - - requestTracker.set(requestId, event) - - // Only log user-initiated requests - if (details.type === 'main_frame' || details.type === 'sub_frame' || details.type === 'xmlhttprequest') { - console.log('🥾 b00t network request start:', details.method, details.url) - } - }, - { urls: [""] } -) - -// Capture request headers -chrome.webRequest.onBeforeSendHeaders.addListener( - (details) => { - const requestId = `${details.requestId}-${details.tabId}` - const event = requestTracker.get(requestId) - - if (event && details.requestHeaders) { - // Store selected headers (avoid sensitive data) - const safeHeaders: Record = {} - details.requestHeaders.forEach(header => { - const name = header.name.toLowerCase() - if (name === 'accept' || name === 'content-type' || name === 'user-agent' || name === 'referer') { - safeHeaders[name] = header.value || '' - } - }) - event.requestHeaders = safeHeaders - } - }, - { urls: [""] }, - ['requestHeaders'] -) - -// Capture response headers and status -chrome.webRequest.onResponseStarted.addListener( - (details) => { - const requestId = `${details.requestId}-${details.tabId}` - const event = requestTracker.get(requestId) - - if (event) { - event.responseStatus = details.statusCode - - if (details.responseHeaders) { - const safeHeaders: Record = {} - details.responseHeaders.forEach(header => { - const name = header.name.toLowerCase() - if (name === 'content-type' || name === 'content-length' || name === 'cache-control') { - safeHeaders[name] = header.value || '' - } - }) - event.responseHeaders = safeHeaders - } - } - }, - { urls: [""] }, - ['responseHeaders'] -) - -// Complete request tracking -chrome.webRequest.onCompleted.addListener( - (details) => { - const requestId = `${details.requestId}-${details.tabId}` - const event = requestTracker.get(requestId) - - if (event && event.timing) { - event.timing.endTime = details.timeStamp - event.timing.duration = details.timeStamp - event.timing.startTime - event.responseStatus = details.statusCode + // Only capture user-initiated requests + if (details.type === 'main_frame' || details.type === 'sub_frame') { + console.log('🥾 b00t network request:', details.method, details.url) - // Store completed request - if (event.type === 'main_frame' || event.type === 'sub_frame' || event.type === 'xmlhttprequest') { - console.log('🥾 b00t network request completed:', event.method, event.url, `${event.timing.duration}ms`) - storeNetworkEvent(event) - } - - requestTracker.delete(requestId) - } - }, - { urls: [""] } -) - -// Handle request errors -chrome.webRequest.onErrorOccurred.addListener( - (details) => { - const requestId = `${details.requestId}-${details.tabId}` - const event = requestTracker.get(requestId) - - if (event && event.timing) { - event.timing.endTime = details.timeStamp - event.timing.duration = details.timeStamp - event.timing.startTime - - // Store failed request with error info - if (event.type === 'main_frame' || event.type === 'sub_frame' || event.type === 'xmlhttprequest') { - console.log('🥾 b00t network request error:', event.method, event.url, details.error) - storeNetworkEvent({ ...event, responseStatus: 0 }) // 0 indicates error - } - - requestTracker.delete(requestId) + // Store network event + storeNetworkEvent({ + timestamp: Date.now(), + url: details.url, + method: details.method, + type: details.type, + tabId: details.tabId + }) } }, { urls: [""] } + // Removed "requestBody" as webRequestBlocking is not available in MV3 ) interface NetworkEvent { @@ -146,25 +39,8 @@ interface NetworkEvent { method: string type: string tabId: number - requestHeaders?: Record - responseStatus?: number - responseHeaders?: Record - initiator?: string - redirectChain?: string[] - timing?: { - startTime: number - endTime?: number - duration?: number - } - size?: { - requestBytes?: number - responseBytes?: number - } } -// Track request timing and response data -const requestTracker = new Map() - async function storeNetworkEvent(event: NetworkEvent) { try { const result = await chrome.storage.local.get(['b00t_network_events']) @@ -186,17 +62,10 @@ async function storeNetworkEvent(event: NetworkEvent) { // Handle messages from content scripts and popup chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === 'GET_EVENTS') { - chrome.storage.local.get(['b00t_events', 'b00t_network_events', 'b00t_screenshots']).then((result) => { + chrome.storage.local.get(['b00t_events', 'b00t_network_events']).then((result) => { sendResponse({ events: result.b00t_events || [], - networkEvents: result.b00t_network_events || [], - screenshots: result.b00t_screenshots || [], - nats: { - connected: natsClient.isConnected(), - operatorId: natsClient.getOperatorId(), - extensionId: natsClient.getExtensionId(), - serverUrl: natsClient.getServerUrl() - } + networkEvents: result.b00t_network_events || [] }) }) return true // Keep message channel open for async response @@ -208,48 +77,8 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }) return true } - - if (request.type === 'CAPTURE_SCREENSHOT') { - captureTabScreenshot(sender.tab?.id).then((screenshot) => { - sendResponse({ - success: !!screenshot, - screenshot: screenshot - }) - }).catch((error) => { - console.warn('🥾 b00t: Screenshot capture failed:', error) - sendResponse({ success: false, error: error.message }) - }) - return true - } }) -// Screenshot capture functionality -async function captureTabScreenshot(tabId?: number): Promise<{dataUrl: string, width: number, height: number} | null> { - try { - if (!tabId) { - throw new Error('No tab ID provided') - } - - // Capture visible tab - const dataUrl = await chrome.tabs.captureVisibleTab(undefined, { - format: 'png', - quality: 90 - }) - - // Get tab dimensions - const tab = await chrome.tabs.get(tabId) - - return { - dataUrl, - width: tab.width || 1920, - height: tab.height || 1080 - } - } catch (error) { - console.warn('🥾 b00t: Tab screenshot failed:', error) - return null - } -} - async function authorizeSite(domain: string) { try { const result = await chrome.storage.local.get(['authorizedSites']) diff --git a/b00t-browser-ext/content.ts b/b00t-browser-ext/content.ts index 19d5437..e362d35 100644 --- a/b00t-browser-ext/content.ts +++ b/b00t-browser-ext/content.ts @@ -1,9 +1,6 @@ // b00t Browser Extension - Content Script // Captures user-initiated navigation and DOM telemetry -import { B00tScreenshotCapture } from "./screenshot" -import { storageManager } from "./storage" - console.log("🥾 b00t browser extension loaded") interface B00tTelemetryEvent { @@ -28,10 +25,8 @@ interface B00tTelemetryEvent { class B00tTelemetryCapture { private enabled: boolean = false private events: B00tTelemetryEvent[] = [] - private screenshotCapture: B00tScreenshotCapture constructor() { - this.screenshotCapture = new B00tScreenshotCapture() this.init() } @@ -65,10 +60,7 @@ class B00tTelemetryCapture { // Filter for navigation-relevant clicks if (this.isNavigationClick(target)) { - // Handle async click capture - this.captureClickEvent(target).catch(error => { - console.warn('🥾 b00t: Click capture failed:', error) - }) + this.captureClickEvent(target) } }, { passive: true }) @@ -76,16 +68,11 @@ class B00tTelemetryCapture { document.addEventListener('submit', (event) => { if (!this.enabled) return - // Handle async form capture - this.captureFormSubmit(event.target as HTMLFormElement).catch(error => { - console.warn('🥾 b00t: Form capture failed:', error) - }) + this.captureFormSubmit(event.target as HTMLFormElement) }, { passive: true }) // Capture page navigation - this.capturePageNavigation().catch(error => { - console.warn('🥾 b00t: Navigation capture failed:', error) - }) + this.capturePageNavigation() } private isNavigationClick(target: HTMLElement): boolean { @@ -100,7 +87,7 @@ class B00tTelemetryCapture { ) } - private async captureClickEvent(target: HTMLElement) { + private captureClickEvent(target: HTMLElement) { const event: B00tTelemetryEvent = { timestamp: Date.now(), url: window.location.href, @@ -116,19 +103,9 @@ class B00tTelemetryCapture { } this.addEvent(event) - - // Capture screenshot for navigation clicks - if (target.tagName === 'A' || target.tagName === 'BUTTON') { - const targetDescription = `${target.tagName.toLowerCase()}${target.id ? '#' + target.id : ''}${target.className ? '.' + target.className.split(' ')[0] : ''}` - const screenshot = await this.screenshotCapture.captureScreenshot('click', targetDescription) - - if (screenshot) { - await this.screenshotCapture.storeScreenshot(screenshot, true) - } - } } - private async captureFormSubmit(form: HTMLFormElement) { + private captureFormSubmit(form: HTMLFormElement) { const event: B00tTelemetryEvent = { timestamp: Date.now(), url: window.location.href, @@ -142,17 +119,9 @@ class B00tTelemetryCapture { } this.addEvent(event) - - // Capture screenshot for form submissions - const formDescription = `form${form.id ? '#' + form.id : ''}${form.className ? '.' + form.className.split(' ')[0] : ''}` - const screenshot = await this.screenshotCapture.captureScreenshot('form_submit', formDescription) - - if (screenshot) { - await this.screenshotCapture.storeScreenshot(screenshot, true) - } } - private async capturePageNavigation() { + private capturePageNavigation() { const event: B00tTelemetryEvent = { timestamp: Date.now(), url: window.location.href, @@ -161,135 +130,14 @@ class B00tTelemetryCapture { } this.addEvent(event) - - // Capture screenshot for page navigation - const screenshot = await this.screenshotCapture.captureScreenshot('navigation') - - if (screenshot) { - await this.screenshotCapture.storeScreenshot(screenshot, true) - } - - // Clean up old screenshots periodically - if (Math.random() < 0.1) { // 10% chance on each navigation - await this.screenshotCapture.cleanupOldScreenshots() - } } private captureDOMState() { return { title: document.title, - url: window.location.href, - timestamp: Date.now(), - viewport: { - width: window.innerWidth, - height: window.innerHeight, - scrollX: window.scrollX, - scrollY: window.scrollY - }, - elements: { - forms: this.analyzeForms(), - buttons: this.analyzeButtons(), - links: this.analyzeLinks(), - inputs: this.analyzeInputs(), - images: document.querySelectorAll('img').length, - iframes: document.querySelectorAll('iframe').length - }, - structure: { - headings: this.analyzeHeadings(), - sections: document.querySelectorAll('section, article, main, aside').length, - navigation: document.querySelectorAll('nav').length - }, - content: { - textLength: document.body.textContent?.length || 0, - hasVideo: document.querySelectorAll('video').length > 0, - hasAudio: document.querySelectorAll('audio').length > 0, - hasCanvas: document.querySelectorAll('canvas').length > 0 - }, - meta: this.extractMetadata(), - accessibility: this.analyzeAccessibility() - } - } - - private analyzeForms() { - const forms = Array.from(document.querySelectorAll('form')) - return forms.map(form => ({ - id: form.id || undefined, - action: form.action || undefined, - method: form.method || 'get', - inputs: form.querySelectorAll('input, select, textarea').length, - hasFileUpload: form.querySelectorAll('input[type="file"]').length > 0 - })) - } - - private analyzeButtons() { - const buttons = Array.from(document.querySelectorAll('button, input[type="button"], input[type="submit"]')) - return buttons.map(btn => ({ - type: btn.tagName.toLowerCase(), - text: btn.textContent?.substring(0, 50) || undefined, - id: btn.id || undefined, - className: btn.className || undefined, - disabled: (btn as HTMLButtonElement).disabled - })) - } - - private analyzeLinks() { - const links = Array.from(document.querySelectorAll('a[href]')) - return links.map(link => ({ - href: (link as HTMLAnchorElement).href, - text: link.textContent?.substring(0, 50) || undefined, - target: (link as HTMLAnchorElement).target || undefined, - isExternal: (link as HTMLAnchorElement).hostname !== window.location.hostname - })) - } - - private analyzeInputs() { - const inputs = Array.from(document.querySelectorAll('input, select, textarea')) - return inputs.map(input => ({ - type: (input as HTMLInputElement).type || input.tagName.toLowerCase(), - name: (input as HTMLInputElement).name || undefined, - id: input.id || undefined, - required: (input as HTMLInputElement).required, - placeholder: (input as HTMLInputElement).placeholder || undefined - })) - } - - private analyzeHeadings() { - const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')) - return headings.map(heading => ({ - level: parseInt(heading.tagName.charAt(1)), - text: heading.textContent?.substring(0, 100) || undefined, - id: heading.id || undefined - })) - } - - private extractMetadata() { - const meta: Record = {} - - // Standard meta tags - const metaTags = document.querySelectorAll('meta[name], meta[property]') - metaTags.forEach(tag => { - const name = tag.getAttribute('name') || tag.getAttribute('property') - const content = tag.getAttribute('content') - if (name && content) { - meta[name] = content.substring(0, 200) // Limit content length - } - }) - - // Page language - const lang = document.documentElement.lang || document.querySelector('html')?.getAttribute('lang') - if (lang) meta.language = lang - - return meta - } - - private analyzeAccessibility() { - return { - hasAltImages: document.querySelectorAll('img[alt]').length, - totalImages: document.querySelectorAll('img').length, - hasAriaLabels: document.querySelectorAll('[aria-label]').length, - hasSkipLinks: document.querySelectorAll('a[href^="#"]').length, - hasHeadings: document.querySelectorAll('h1, h2, h3, h4, h5, h6').length > 0, - hasLandmarks: document.querySelectorAll('main, nav, aside, section').length + forms: document.querySelectorAll('form').length, + buttons: document.querySelectorAll('button, input[type="button"], input[type="submit"]').length, + links: document.querySelectorAll('a[href]').length } } @@ -309,8 +157,9 @@ class B00tTelemetryCapture { private async storeEvents() { try { - await storageManager.storeEvents(this.events.slice(-10), 'b00t_events') // Store last 10 events - console.log(`🥾 b00t: Stored ${this.events.length} events via enhanced storage`) + await chrome.storage.local.set({ + 'b00t_events': this.events + }) } catch (error) { console.warn('🥾 b00t: Failed to store events', error) } diff --git a/b00t-browser-ext/plasmo.config.ts b/b00t-browser-ext/plasmo.config.ts index 2b2cfae..4a7de63 100644 --- a/b00t-browser-ext/plasmo.config.ts +++ b/b00t-browser-ext/plasmo.config.ts @@ -6,8 +6,7 @@ const config: PlasmoConfig = { "activeTab", "webRequest", "storage", - "tabs", - "tabCapture" + "tabs" ], host_permissions: [ "" diff --git a/b00t-browser-ext/popup.tsx b/b00t-browser-ext/popup.tsx index c07312c..7c59d0a 100644 --- a/b00t-browser-ext/popup.tsx +++ b/b00t-browser-ext/popup.tsx @@ -1,86 +1,15 @@ -import { useState, useEffect } from "react" +import { useState } from "react" import "./style.css" -interface TelemetryStats { - events: number - networkEvents: number - screenshots: number - totalSize: number - enabled: boolean - currentSite: string -} - function IndexPopup() { const [data, setData] = useState("") - const [stats, setStats] = useState({ - events: 0, - networkEvents: 0, - screenshots: 0, - totalSize: 0, - enabled: false, - currentSite: "" - }) - - useEffect(() => { - // Get current tab info and telemetry stats - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - const currentTab = tabs[0] - const currentSite = currentTab?.url ? new URL(currentTab.url).hostname : "unknown" - - // Get telemetry data - chrome.runtime.sendMessage({ type: 'GET_EVENTS' }, (response) => { - if (response) { - setStats({ - events: response.events?.length || 0, - networkEvents: response.networkEvents?.length || 0, - screenshots: response.screenshots?.length || 0, - totalSize: JSON.stringify(response).length, - enabled: true, // TODO: check actual site authorization - currentSite - }) - } - }) - }) - }, []) - - const authorizeSite = () => { - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - const currentTab = tabs[0] - if (currentTab?.url) { - const domain = new URL(currentTab.url).hostname - chrome.runtime.sendMessage({ - type: 'AUTHORIZE_SITE', - domain - }, (response) => { - if (response?.success) { - setData(`✅ Authorized for ${domain}`) - setStats(prev => ({ ...prev, enabled: true })) - } - }) - } - }) - } - - const clearData = () => { - chrome.storage.local.clear(() => { - setData("🗑️ All telemetry data cleared") - setStats(prev => ({ - ...prev, - events: 0, - networkEvents: 0, - screenshots: 0, - totalSize: 0 - })) - }) - } return (

🥾 b00t Browser Extension

Operator telemetry and agent integration

@@ -93,103 +22,36 @@ function IndexPopup() { width: 10, height: 10, borderRadius: "50%", - backgroundColor: stats.enabled ? "#10b981" : "#ef4444" + backgroundColor: "#10b981" }} /> - - {stats.enabled ? `Recording on ${stats.currentSite}` : "Not authorized"} - + Ready to capture
-

Telemetry Data

-
-
📊 Events: {stats.events}
-
🌐 Network: {stats.networkEvents}
-
📸 Screenshots: {stats.screenshots}
-
💾 Size: {(stats.totalSize / 1024).toFixed(1)} KB
-
-
- -
-

NATS Connection

-
-
- - {stats.nats?.connected ? "Connected" : "Discovering server..."} - -
- {stats.nats?.serverUrl && ( -
- Server: {new URL(stats.nats.serverUrl).host} -
- )} - {stats.nats?.operatorId && ( -
- Operator: {stats.nats.operatorId.slice(0, 16)}... -
- )} - {stats.nats?.extensionId && ( -
- Extension: {stats.nats.extensionId.slice(0, 16)}... -
- )} -
- -
+

Site Authorization

- -
{data && ( -
+
{data}
)} - -
- Phase 2b Complete: DOM Analysis • Network Monitoring • Visual Snapshots • NATS Command/Control -
) } diff --git a/justfile b/justfile index 580dbfd..4307873 100644 --- a/justfile +++ b/justfile @@ -174,6 +174,29 @@ validate-mcp: cd {{repo-root}}/_b00t_ taplo lint --schema file://$PWD/schema-资源/mcp.json *.mcp.toml +# Build and package b00t browser extension +browser-ext-build: + #!/bin/bash + echo "🥾 Building b00t browser extension..." + cd {{repo-root}}/b00t-browser-ext + npm ci + npm run build + echo "✅ Extension built in build/chrome-mv3-prod/" + +browser-ext-package: + #!/bin/bash + echo "📦 Packaging b00t browser extension..." + cd {{repo-root}}/b00t-browser-ext + npm run package + VERSION=$(node -p "require('./package.json').version") + echo "✅ Extension packaged as b00t-browser-ext-chrome-v${VERSION}.zip" + +browser-ext-dev: + #!/bin/bash + echo "🚀 Starting b00t browser extension dev server..." + cd {{repo-root}}/b00t-browser-ext + npm run dev + socks5: docker run -d -p 1080:1080 -v ./koblas.toml:/etc/koblas/config.toml -e RUST_LOG=debug -e KOBLAS_NO_AUTHENTICATION=true -e KOBLAS_ANONYMIZE=false --name koblas docker.io/ynuwenhof/koblas:latest From a71bfaf9b70506e4ced351105bf9d2f7ef14a93f Mon Sep 17 00:00:00 2001 From: Brian H Date: Fri, 22 Aug 2025 12:14:11 +0000 Subject: [PATCH 4/4] feat: acp from sm3llyd0s --- .../Cargo.toml | 4 + .../src/agent.rs | 76 +++- .../src/error.rs | 5 + .../src/lib.rs | 21 +- .../src/security.rs | 393 ++++++++++++++++++ b00t-mcp/src/acp_hive.rs | 22 +- b00t-mcp/src/acp_tools.rs | 103 ++++- 7 files changed, 597 insertions(+), 27 deletions(-) create mode 100644 b00t-lib-agent-coordination-protocol-rs/src/security.rs diff --git a/b00t-lib-agent-coordination-protocol-rs/Cargo.toml b/b00t-lib-agent-coordination-protocol-rs/Cargo.toml index d72d0c7..70866dc 100644 --- a/b00t-lib-agent-coordination-protocol-rs/Cargo.toml +++ b/b00t-lib-agent-coordination-protocol-rs/Cargo.toml @@ -26,6 +26,10 @@ chrono = { version = "0.4", features = ["serde"] } tracing = "0.1" anyhow = "1.0" futures = "0.3" +jsonwebtoken = "9.0" +sha2 = "0.10" +hex = "0.4" +reqwest = { version = "0.12", features = ["json"] } # Python bindings - using compatible version pyo3 = { version = "0.25", features = ["extension-module"], optional = true } diff --git a/b00t-lib-agent-coordination-protocol-rs/src/agent.rs b/b00t-lib-agent-coordination-protocol-rs/src/agent.rs index 9d5bfa1..be5f87f 100644 --- a/b00t-lib-agent-coordination-protocol-rs/src/agent.rs +++ b/b00t-lib-agent-coordination-protocol-rs/src/agent.rs @@ -2,7 +2,7 @@ use crate::{ ACPMessage, MessageType, StepBarrier, NatsTransport, - ACPError, Result, JsonValue + ACPError, Result, JsonValue, AcpJwtValidator, AcpSecurityContext, NamespaceEnforcer }; use crate::transport::NatsConfig; use std::collections::HashMap; @@ -19,7 +19,7 @@ pub struct AgentConfig { pub agent_id: String, /// NATS server URL pub nats_url: String, - /// GitHub user namespace (e.g., "account.elasticdotventures") + /// Hive namespace (e.g., "account.{hive}.{role}") pub namespace: String, /// JWT token for NATS authentication pub jwt_token: Option, @@ -30,13 +30,22 @@ pub struct AgentConfig { } impl AgentConfig { - /// Create new agent config + /// Create new agent config with environment variable support pub fn new(agent_id: String, nats_url: String, namespace: String) -> Self { + let nats_url = if nats_url.is_empty() { + std::env::var("NATS_URL") + .unwrap_or_else(|_| "nats://c010.promptexecution.com:4222".to_string()) + } else { + nats_url + }; + + let jwt_token = std::env::var("B00T_HIVE_JWT").ok(); + Self { agent_id, nats_url, namespace, - jwt_token: None, + jwt_token, role: "ai-assistant".to_string(), timeout_ms: 30000, } @@ -60,6 +69,11 @@ impl AgentConfig { self } + /// Create agent config using environment variables for NATS and JWT + pub fn from_env(agent_id: String, namespace: String) -> Self { + Self::new(agent_id, String::new(), namespace) + } + /// Validate configuration pub fn validate(&self) -> Result<()> { if self.agent_id.is_empty() { @@ -85,6 +99,10 @@ pub struct Agent { step_barrier: Arc>, message_handlers: Arc>>>, running: Arc>, + /// Security context for JWT-based namespace enforcement + security_context: Option, + /// Namespace enforcer for validating operations + namespace_enforcer: Option, } impl Agent { @@ -105,6 +123,40 @@ impl Agent { StepBarrier::new(vec![config.agent_id.clone()], config.timeout_ms) )); + // Initialize security context if JWT is provided + let (security_context, namespace_enforcer) = if let Some(jwt_token) = &config.jwt_token { + info!("Validating JWT token for agent '{}'", config.agent_id); + + // For development, we'll use a placeholder validator + // In production, this would use the actual operator JWT + let validator = AcpJwtValidator::new("placeholder_secret".to_string()); + + match validator.validate_jwt(jwt_token) { + Ok(security_ctx) => { + // Verify namespace matches config + if security_ctx.namespace != config.namespace { + return Err(ACPError::authentication_failed(format!( + "JWT namespace '{}' does not match config namespace '{}'", + security_ctx.namespace, config.namespace + ))); + } + + let enforcer = NamespaceEnforcer::new(security_ctx.clone()); + info!("Agent '{}' authenticated for namespace '{}'", config.agent_id, security_ctx.namespace); + (Some(security_ctx), Some(enforcer)) + }, + Err(e) => { + warn!("JWT validation failed for agent '{}': {}", config.agent_id, e); + // For development, continue without security context + // In production, this should be an error + (None, None) + } + } + } else { + info!("No JWT provided for agent '{}' - running in development mode", config.agent_id); + (None, None) + }; + info!("Agent '{}' initialized", config.agent_id); Ok(Self { @@ -113,6 +165,8 @@ impl Agent { step_barrier, message_handlers: Arc::new(Mutex::new(HashMap::new())), running: Arc::new(Mutex::new(false)), + security_context, + namespace_enforcer, }) } @@ -424,4 +478,18 @@ mod tests { assert_eq!(config.role, "ci-cd"); assert_eq!(config.timeout_ms, 60000); } + + #[test] + fn test_agent_config_from_env() { + // Test environment variable fallback + let config = AgentConfig::from_env( + "test-agent".to_string(), + "account.test".to_string() + ); + + assert_eq!(config.agent_id, "test-agent"); + assert_eq!(config.namespace, "account.test"); + // NATS URL should fall back to default since no env var is set in test + assert!(config.nats_url.contains("c010.promptexecution.com")); + } } \ No newline at end of file diff --git a/b00t-lib-agent-coordination-protocol-rs/src/error.rs b/b00t-lib-agent-coordination-protocol-rs/src/error.rs index a22f5fc..479af91 100644 --- a/b00t-lib-agent-coordination-protocol-rs/src/error.rs +++ b/b00t-lib-agent-coordination-protocol-rs/src/error.rs @@ -93,6 +93,11 @@ impl ACPError { Self::AuthenticationFailed { reason: reason.into() } } + /// Create an authentication failed error (alias) + pub fn authentication_failed(reason: impl Into) -> Self { + Self::AuthenticationFailed { reason: reason.into() } + } + /// Create a permission denied error pub fn permission_denied(reason: impl Into) -> Self { Self::PermissionDenied { reason: reason.into() } diff --git a/b00t-lib-agent-coordination-protocol-rs/src/lib.rs b/b00t-lib-agent-coordination-protocol-rs/src/lib.rs index 26e8fe5..9035634 100644 --- a/b00t-lib-agent-coordination-protocol-rs/src/lib.rs +++ b/b00t-lib-agent-coordination-protocol-rs/src/lib.rs @@ -23,14 +23,14 @@ //! //! #[tokio::main] //! async fn main() -> Result<(), Box> { -//! let config = AgentConfig { -//! agent_id: "claude.124435".to_string(), -//! nats_url: "nats://c010.promptexecution.com:4222".to_string(), -//! namespace: "account.elasticdotventures".to_string(), -//! jwt_token: Some("eyJ0eXAi...".to_string()), -//! role: "ai-assistant".to_string(), -//! timeout_ms: 30000, -//! }; +//! // Set environment variables: +//! // NATS_URL=nats://c010.promptexecution.com:4222 +//! // B00T_HIVE_JWT=eyJ0eXAi... +//! +//! let config = AgentConfig::from_env( +//! "claude.124435".to_string(), +//! "account.elasticdotventures".to_string() +//! ); //! //! let mut agent = Agent::new(config).await?; //! @@ -54,6 +54,7 @@ pub mod agent; pub mod protocol; pub mod transport; pub mod error; +pub mod security; #[cfg(feature = "python")] pub mod python; @@ -65,6 +66,10 @@ pub use agent::{Agent, AgentConfig}; pub use protocol::{ACPMessage, MessageType, StepBarrier}; pub use transport::{NatsTransport, NatsConfig}; pub use error::{ACPError, Result}; +pub use security::{ + AcpJwtValidator, AcpSecurityContext, NamespaceEnforcer, + SubjectOperation, fetch_jwt_from_website +}; // Re-export commonly used types pub use serde_json::Value as JsonValue; diff --git a/b00t-lib-agent-coordination-protocol-rs/src/security.rs b/b00t-lib-agent-coordination-protocol-rs/src/security.rs new file mode 100644 index 0000000..ef59954 --- /dev/null +++ b/b00t-lib-agent-coordination-protocol-rs/src/security.rs @@ -0,0 +1,393 @@ +//! JWT Security and Namespace Enforcement for ACP +//! +//! Integrates with b00t-website JWT provisioning to enforce namespace isolation +//! based on GitHub user identity. + +use anyhow::{Result, Context}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use chrono::{DateTime, Utc}; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; + +/// JWT Claims structure matching b00t-website NATS provisioner +#[derive(Debug, Serialize, Deserialize)] +pub struct AcpJwtClaims { + /// Subject: acp.{hive}.{role} for hive JWTs + pub sub: String, + /// Audience: github.{hive} + pub aud: String, + /// Issued at timestamp + pub iat: u64, + /// Expiration timestamp + pub exp: u64, + /// NATS-specific claims + pub nats: NatsPermissions, + /// ACP-specific claims + pub acp: Option, +} + +/// ACP-specific claims for hive operations +#[derive(Debug, Serialize, Deserialize)] +pub struct AcpPermissions { + /// Token type ("hive") + #[serde(rename = "type")] + pub token_type: String, + /// Hive namespace (account.{hive}.{role}) + pub namespace: String, + /// Agent role + pub role: String, + /// ACP permissions + pub permissions: Vec, + /// Issuer + pub issued_by: String, +} + +/// NATS permissions embedded in JWT +#[derive(Debug, Serialize, Deserialize)] +pub struct NatsPermissions { + /// User type (always "user" for ACP agents) + #[serde(rename = "type")] + pub user_type: String, + /// NATS JWT version + pub version: u8, + /// Subscription limits (-1 = unlimited) + pub subs: i32, + /// Data limits (-1 = unlimited) + pub data: i32, + /// Payload limits (-1 = unlimited) + pub payload: i32, + /// Connection restrictions + pub connect_only: bool, + /// Subject permissions + pub permissions: SubjectPermissions, +} + +/// Subject-based permissions for NATS +#[derive(Debug, Serialize, Deserialize)] +pub struct SubjectPermissions { + /// Subjects this user can publish to + pub publish: Vec, + /// Subjects this user can subscribe to + pub subscribe: Vec, +} + +/// Security context for ACP operations +#[derive(Debug, Clone)] +pub struct AcpSecurityContext { + /// GitHub user ID (hive identifier) extracted from JWT + pub hive: String, + /// Hive's allowed namespace (account.{hive}.{role}) + pub namespace: String, + /// Agent role from JWT subject + pub role: String, + /// Process ID from JWT subject (or "hive" for hive tokens) + pub pid: String, + /// JWT expiration time + pub expires_at: DateTime, + /// Allowed publish subjects + pub publish_subjects: Vec, + /// Allowed subscribe subjects + pub subscribe_subjects: Vec, +} + +/// JWT validator for ACP operations +pub struct AcpJwtValidator { + /// Signing secret derived from operator JWT (matches b00t-website) + signing_secret: String, +} + +impl AcpJwtValidator { + /// Create new JWT validator with signing secret + pub fn new(signing_secret: String) -> Self { + Self { signing_secret } + } + + /// Create validator by deriving secret from operator JWT (matches b00t-website logic) + pub fn from_operator_jwt(operator_jwt: &str) -> Result { + let signing_secret = Self::derive_signing_secret(operator_jwt)?; + Ok(Self::new(signing_secret)) + } + + /// Validate JWT and extract security context + pub fn validate_jwt(&self, jwt_token: &str) -> Result { + // Set up JWT validation parameters + let mut validation = Validation::new(Algorithm::HS256); + validation.validate_exp = true; + validation.validate_aud = false; // We'll validate audience manually + + // Decode and validate JWT + let decoding_key = DecodingKey::from_secret(self.signing_secret.as_bytes()); + let token_data = decode::(jwt_token, &decoding_key, &validation) + .context("Failed to decode JWT")?; + + let claims = token_data.claims; + + // Parse subject: Handle both user.{hive}.{role}.{pid} and acp.{hive}.{role} + let subject_parts: Vec<&str> = claims.sub.split('.').collect(); + let (hive, role, pid, namespace) = if subject_parts.len() == 4 && subject_parts[0] == "user" { + // Standard user token: user.{hive}.{role}.{pid} + let hive = subject_parts[1].to_string(); + let role = subject_parts[2].to_string(); + let pid = subject_parts[3].to_string(); + let namespace = format!("account.{}.{}", hive, role); + (hive, role, pid, namespace) + } else if subject_parts.len() == 3 && subject_parts[0] == "acp" { + // ACP hive token: acp.{hive}.{role} + let hive = subject_parts[1].to_string(); + let role = subject_parts[2].to_string(); + let pid = "hive".to_string(); // Use "hive" as pseudo-PID for hive tokens + let namespace = format!("account.{}.{}", hive, role); + (hive, role, pid, namespace) + } else { + return Err(anyhow::anyhow!("Invalid JWT subject format: {}", claims.sub)); + }; + + // Validate audience matches GitHub hive pattern + let expected_audience = format!("github.{}", hive); + if claims.aud != expected_audience { + return Err(anyhow::anyhow!("JWT audience mismatch: expected {}, got {}", expected_audience, claims.aud)); + } + + // Convert expiration timestamp + let expires_at = DateTime::from_timestamp(claims.exp as i64, 0) + .ok_or_else(|| anyhow::anyhow!("Invalid expiration timestamp"))?; + + // Validate permissions are for the correct namespace + Self::validate_namespace_permissions(&namespace, &claims.nats.permissions)?; + + Ok(AcpSecurityContext { + hive, + namespace, + role, + pid, + expires_at, + publish_subjects: claims.nats.permissions.publish, + subscribe_subjects: claims.nats.permissions.subscribe, + }) + } + + /// Validate that all permissions are within the user's namespace + fn validate_namespace_permissions(namespace: &str, permissions: &SubjectPermissions) -> Result<()> { + // Check publish permissions + for subject in &permissions.publish { + if !Self::is_subject_in_namespace(subject, namespace) { + return Err(anyhow::anyhow!("Publish permission '{}' outside namespace '{}'", subject, namespace)); + } + } + + // Check subscribe permissions + for subject in &permissions.subscribe { + if !Self::is_subject_in_namespace(subject, namespace) { + return Err(anyhow::anyhow!("Subscribe permission '{}' outside namespace '{}'", subject, namespace)); + } + } + + Ok(()) + } + + /// Check if a subject pattern is within the allowed namespace + fn is_subject_in_namespace(subject: &str, namespace: &str) -> bool { + // Allow exact matches and wildcard patterns within namespace + subject.starts_with(namespace) || + subject.starts_with(&format!("{}.", namespace)) || + subject == format!("{}.>", namespace) + } + + /// Derive signing secret from operator JWT (matches b00t-website logic) + fn derive_signing_secret(operator_jwt: &str) -> Result { + use sha2::{Digest, Sha256}; + + let salt = "b00t-nats-user-jwt-salt"; + let combined = format!("{}{}", operator_jwt, salt); + + let mut hasher = Sha256::new(); + hasher.update(combined.as_bytes()); + let result = hasher.finalize(); + + Ok(hex::encode(result)) + } +} + +/// Namespace enforcement for ACP hive operations +pub struct NamespaceEnforcer { + security_context: AcpSecurityContext, +} + +impl NamespaceEnforcer { + /// Create new namespace enforcer with security context + pub fn new(security_context: AcpSecurityContext) -> Self { + Self { security_context } + } + + /// Validate that a hive mission is within hive's namespace + pub fn validate_mission_access(&self, mission_id: &str, namespace: &str) -> Result<()> { + // Ensure namespace matches hive's allowed namespace + if namespace != self.security_context.namespace { + return Err(anyhow::anyhow!( + "Access denied: mission namespace '{}' does not match hive namespace '{}'", + namespace, + self.security_context.namespace + )); + } + + // Additional validation: mission ID should not try to escape namespace + if mission_id.contains("..") || mission_id.contains("/") || mission_id.contains("\\") { + return Err(anyhow::anyhow!("Invalid mission ID: contains path traversal characters")); + } + + Ok(()) + } + + /// Validate that an ACP subject is allowed for this hive + pub fn validate_subject_access(&self, subject: &str, operation: SubjectOperation) -> Result<()> { + let allowed_subjects = match operation { + SubjectOperation::Publish => &self.security_context.publish_subjects, + SubjectOperation::Subscribe => &self.security_context.subscribe_subjects, + }; + + // Check if subject matches any allowed pattern + for pattern in allowed_subjects { + if Self::subject_matches_pattern(subject, pattern) { + return Ok(()); + } + } + + Err(anyhow::anyhow!( + "Access denied: {} operation on subject '{}' not permitted for hive '{}'", + match operation { + SubjectOperation::Publish => "publish", + SubjectOperation::Subscribe => "subscribe", + }, + subject, + self.security_context.hive + )) + } + + /// Check if a subject matches a NATS pattern (with > and * wildcards) + fn subject_matches_pattern(subject: &str, pattern: &str) -> bool { + if pattern.ends_with(".>") { + // Multi-level wildcard + let prefix = &pattern[..pattern.len() - 2]; + subject.starts_with(prefix) + } else if pattern.contains('*') { + // Single-level wildcard - simplified matching + let pattern_parts: Vec<&str> = pattern.split('.').collect(); + let subject_parts: Vec<&str> = subject.split('.').collect(); + + if pattern_parts.len() != subject_parts.len() { + return false; + } + + for (p_part, s_part) in pattern_parts.iter().zip(subject_parts.iter()) { + if *p_part != "*" && *p_part != *s_part { + return false; + } + } + true + } else { + // Exact match + subject == pattern + } + } + + /// Get hive's security context + pub fn security_context(&self) -> &AcpSecurityContext { + &self.security_context + } +} + +/// Type of subject operation for permission checking +#[derive(Debug, Clone, Copy)] +pub enum SubjectOperation { + Publish, + Subscribe, +} + +/// Helper function to fetch ACP Hive JWT from b00t-website +pub async fn fetch_jwt_from_website( + website_url: &str, + session_token: &str, + role: &str, +) -> Result { + let client = reqwest::Client::new(); + + let response = client + .post(&format!("{}/api/nats/hive-jwt", website_url)) + .header("Cookie", format!("session_token={}", session_token)) + .json(&serde_json::json!({ + "role": role + })) + .send() + .await + .context("Failed to request ACP Hive JWT from b00t-website")?; + + if !response.status().is_success() { + let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string()); + return Err(anyhow::anyhow!("ACP Hive JWT request failed: {} - {}", response.status(), error_text)); + } + + let response_data: serde_json::Value = response.json().await + .context("Failed to parse ACP Hive JWT response")?; + + let jwt = response_data + .get("jwt") + .and_then(|j| j.as_str()) + .ok_or_else(|| anyhow::anyhow!("ACP Hive JWT not found in response"))?; + + Ok(jwt.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_subject_pattern_matching() { + // Test exact match + assert!(NamespaceEnforcer::subject_matches_pattern( + "account.alice.test", + "account.alice.test" + )); + + // Test multi-level wildcard + assert!(NamespaceEnforcer::subject_matches_pattern( + "account.alice.test.deep", + "account.alice.>" + )); + + // Test single-level wildcard + assert!(NamespaceEnforcer::subject_matches_pattern( + "account.alice.test", + "account.alice.*" + )); + + // Test no match + assert!(!NamespaceEnforcer::subject_matches_pattern( + "account.bob.test", + "account.alice.*" + )); + } + + #[test] + fn test_namespace_validation() { + assert!(AcpJwtValidator::is_subject_in_namespace( + "account.alice.test", + "account.alice" + )); + + assert!(AcpJwtValidator::is_subject_in_namespace( + "account.alice.>", + "account.alice" + )); + + assert!(!AcpJwtValidator::is_subject_in_namespace( + "account.bob.test", + "account.alice" + )); + + assert!(!AcpJwtValidator::is_subject_in_namespace( + "global.system.test", + "account.alice" + )); + } +} \ No newline at end of file diff --git a/b00t-mcp/src/acp_hive.rs b/b00t-mcp/src/acp_hive.rs index db0daf1..e344c9f 100644 --- a/b00t-mcp/src/acp_hive.rs +++ b/b00t-mcp/src/acp_hive.rs @@ -18,7 +18,7 @@ use b00t_acp::{Agent, AgentConfig, ACPMessage, MessageType}; pub struct HiveMission { /// Unique mission identifier pub mission_id: String, - /// Mission namespace (e.g., account.username) + /// Mission namespace (e.g., account.{hive}.{role}) pub namespace: String, /// Expected number of agents in the hive pub expected_agents: usize, @@ -67,6 +67,8 @@ pub struct AcpHiveClient { mission: HiveMission, agent_status: AgentStatus, hive_status: HiveStatus, + /// JWT token for NATS authentication and namespace enforcement + jwt_token: Option, } impl AcpHiveClient { @@ -119,6 +121,7 @@ impl AcpHiveClient { mission, agent_status, hive_status, + jwt_token: None, }) } @@ -340,6 +343,23 @@ impl AcpHiveClient { pub fn current_step(&self) -> u64 { self.agent_status.step } + + /// Set JWT token for NATS authentication + pub fn set_jwt_token(&mut self, jwt_token: String) { + self.jwt_token = Some(jwt_token); + info!("🔐 JWT token set for agent {} in mission {}", + self.agent_status.agent_id, self.mission.mission_id); + } + + /// Get JWT token + pub fn jwt_token(&self) -> Option<&String> { + self.jwt_token.as_ref() + } + + /// Check if agent is authenticated with JWT + pub fn is_authenticated(&self) -> bool { + self.jwt_token.is_some() + } } /// Helper functions for MCP tool integration diff --git a/b00t-mcp/src/acp_tools.rs b/b00t-mcp/src/acp_tools.rs index e2798fc..3ed73f9 100644 --- a/b00t-mcp/src/acp_tools.rs +++ b/b00t-mcp/src/acp_tools.rs @@ -10,6 +10,7 @@ use tokio::sync::Mutex; use tracing::{info, warn, error}; use crate::acp_hive::{AcpHiveClient, HiveMission}; +use b00t_acp::{fetch_jwt_from_website, AcpJwtValidator}; /// Global hive client registry for MCP agents type HiveRegistry = Arc>>; @@ -92,22 +93,31 @@ pub struct HiveShowParams { pub async fn acp_hive_join(params: JoinHiveParams) -> Result { let agent_id = format!("mcp_agent_{}", uuid::Uuid::new_v4().to_string()[..8]); let namespace = params.namespace.unwrap_or_else(|| { - format!("account.{}", whoami::username()) + get_hive_namespace(¶ms.role) }); let nats_url = params.nats_url.unwrap_or_else(|| { - "nats://c010.promptexecution.com:4222".to_string() + std::env::var("NATS_URL") + .unwrap_or_else(|_| "nats://c010.promptexecution.com:4222".to_string()) }); info!("🐝 MCP agent {} joining hive mission: {}", agent_id, params.mission_id); - // Create hive client - let client = AcpHiveClient::join_mission( + // 🔐 JWT Security: Fetch JWT from b00t-website for namespace authentication + let jwt_token = fetch_jwt_for_hive_operation(¶ms.role, &namespace).await?; + + // Create hive client with JWT authentication + let mut client = AcpHiveClient::join_mission( agent_id.clone(), params.role.clone(), params.mission_id.clone(), - namespace, + namespace.clone(), nats_url, ).await.context("Failed to join hive mission")?; + + // Set JWT token for security + if jwt_token != "development_mode_no_jwt" { + client.set_jwt_token(jwt_token); + } // Register client globally { @@ -121,7 +131,11 @@ pub async fn acp_hive_join(params: JoinHiveParams) -> Result { "agent_id": agent_id, "mission_id": params.mission_id, "role": params.role, - "namespace": namespace + "namespace": namespace, + "security": { + "jwt_authenticated": jwt_token != "development_mode_no_jwt", + "namespace_enforced": jwt_token != "development_mode_no_jwt" + } }); Ok(serde_json::to_string_pretty(&response)?) @@ -131,14 +145,18 @@ pub async fn acp_hive_join(params: JoinHiveParams) -> Result { pub async fn acp_hive_create(params: CreateHiveParams) -> Result { let agent_id = format!("mcp_leader_{}", uuid::Uuid::new_v4().to_string()[..8]); let namespace = params.namespace.unwrap_or_else(|| { - format!("account.{}", whoami::username()) + get_hive_namespace(¶ms.role) }); let nats_url = params.nats_url.unwrap_or_else(|| { - "nats://c010.promptexecution.com:4222".to_string() + std::env::var("NATS_URL") + .unwrap_or_else(|_| "nats://c010.promptexecution.com:4222".to_string()) }); info!("🐝 MCP agent {} creating hive mission: {}", agent_id, params.mission_id); + // 🔐 JWT Security: Fetch JWT from b00t-website for namespace authentication + let jwt_token = fetch_jwt_for_hive_operation(¶ms.role, &namespace).await?; + // Create mission let mission = AcpHiveClient::create_mission( params.mission_id.clone(), @@ -147,13 +165,18 @@ pub async fn acp_hive_create(params: CreateHiveParams) -> Result { params.description.clone(), ); - // Create hive client - let client = AcpHiveClient::new( + // Create hive client with JWT authentication + let mut client = AcpHiveClient::new( agent_id.clone(), params.role.clone(), mission, nats_url, ).await.context("Failed to create hive mission")?; + + // Set JWT token for security + if jwt_token != "development_mode_no_jwt" { + client.set_jwt_token(jwt_token.clone()); + } // Register client globally { @@ -169,7 +192,11 @@ pub async fn acp_hive_create(params: CreateHiveParams) -> Result { "expected_agents": params.expected_agents, "role": params.role, "namespace": namespace, - "description": params.description + "description": params.description, + "security": { + "jwt_authenticated": jwt_token != "development_mode_no_jwt", + "namespace_enforced": jwt_token != "development_mode_no_jwt" + } }); Ok(serde_json::to_string_pretty(&response)?) @@ -354,9 +381,57 @@ pub fn get_nats_url() -> String { .unwrap_or_else(|_| "nats://c010.promptexecution.com:4222".to_string()) } -/// Helper to get current user namespace -pub fn get_user_namespace() -> String { - format!("account.{}", whoami::username()) +/// Helper to get current hive namespace for a given role +pub fn get_hive_namespace(role: &str) -> String { + format!("account.{}.{}", whoami::username(), role) +} + +/// 🔐 Fetch JWT token for hive operations with namespace enforcement +async fn fetch_jwt_for_hive_operation(role: &str, namespace: &str) -> Result { + // Check for environment variables first (for testing/development) + if let Ok(jwt) = std::env::var("B00T_HIVE_JWT") { + info!("Using JWT from B00T_HIVE_JWT environment variable"); + return Ok(jwt); + } + + if let Ok(session_token) = std::env::var("B00T_SESSION_TOKEN") { + let website_url = std::env::var("B00T_WEBSITE_URL") + .unwrap_or_else(|_| "https://b00t.promptexecution.com".to_string()); + + info!("Fetching JWT from b00t-website for role: {}, namespace: {}", role, namespace); + + match fetch_jwt_from_website(&website_url, &session_token, role).await { + Ok(jwt) => { + // Validate JWT and ensure namespace matches + if let Ok(validator) = AcpJwtValidator::from_operator_jwt("placeholder") { + if let Ok(security_ctx) = validator.validate_jwt(&jwt) { + if security_ctx.namespace == namespace { + info!("✅ JWT validated for namespace: {}", namespace); + return Ok(jwt); + } else { + return Err(anyhow::anyhow!( + "JWT namespace '{}' does not match requested namespace '{}'", + security_ctx.namespace, namespace + )); + } + } + } + + // Return JWT even if validation fails (for development) + warn!("JWT validation failed, continuing in development mode"); + return Ok(jwt); + } + Err(e) => { + warn!("Failed to fetch JWT from b00t-website: {}", e); + } + } + } + + // For development: continue without JWT + warn!("No JWT available - running in development mode (INSECURE)"); + warn!("Set B00T_HIVE_JWT or B00T_SESSION_TOKEN environment variable for production"); + + Ok("development_mode_no_jwt".to_string()) } #[cfg(test)]