//! The workspaces `memoryd` reads (M5c spec, sections 3, 8 and 9): a reader for each //! `[[honcho.read]]` workspace, the fleet's and the agent's own two, each with its own key, a //! short wait for searches and a long one for chat; the label its results carry; what `recall` //! searches and the names `ask_memory` may use. Against a fake Honcho. Do not edit. #[path = "support/honcho.rs"] mod honcho; use std::collections::BTreeMap; use std::ffi::OsString; use std::time::Duration; use bxcore::DataClass; use bxcore::secrets::SecretSource; use honcho::{FakeHoncho, Mode}; use memoryd::honcho::client::{CHAT_READ, CONNECT, classify}; use memoryd::honcho::config::{ FLEET_KEY, HonchoConfig, HonchoSection, RESERVED, ReadSection, ReadWorkspace, TRUSTED_KEY, Trust, check, read_key, }; use memoryd::honcho::read::{ FLEET, Label, ReadTimeouts, Reader, Readers, SEARCH_READ, Source, UNTRUSTED, }; use roncho::MessageSearch; const FAST: ReadTimeouts = ReadTimeouts { connect: Duration::from_secs(5), search: Duration::from_millis(300), chat: Duration::from_millis(900), }; /// Key values built from pieces, so that no file holds one whole. fn key(which: &str) -> String { ["hk", which, "7a31c0"].join("-") } fn read(agent: &str, trust: Trust, class: DataClass) -> ReadWorkspace { ReadWorkspace { agent: agent.to_string(), workspace: format!("{agent}-trusted"), trust, class, } } /// `boxmaker0`, reading `boxmaker1` (labelled trusted, `secret`) and `boxmaker2` (the defaults). /// Keys for `boxmaker1`, the fleet and the own trusted workspace; none for the others. fn config(url: &str) -> HonchoConfig { let mut keys = BTreeMap::new(); for (name, var) in [ (read_key("boxmaker1"), "K_READ_1"), (FLEET_KEY.to_string(), "K_FLEET"), (TRUSTED_KEY.to_string(), "K_TRUSTED"), ] { keys.insert(name, SecretSource::Env(var.to_string())); } HonchoConfig { url: url.to_string(), agent: "boxmaker0".to_string(), owner: "kyle".to_string(), trusted: "boxmaker0-trusted".to_string(), untrusted: "boxmaker0-untrusted".to_string(), fleet: "fleet".to_string(), read: vec![ read("boxmaker1", Trust::Trusted, DataClass::Secret), read("boxmaker2", Trust::Untrusted, DataClass::Public), ], keys, } } fn env(name: &str) -> Option { match name { "K_READ_1" => Some(OsString::from(key("read1"))), "K_FLEET" => Some(OsString::from(key("fleet"))), "K_TRUSTED" => Some(OsString::from(key("trusted"))), _ => None, } } fn readers(url: &str) -> Readers { Readers::new(&config(url), &env, FAST).unwrap().0 } /// Every reader: `recall`'s, then the own two. fn all(r: &Readers) -> Vec<&Reader> { let mut all = r.recall_sources(); all.push(r.resolve("own").unwrap()); all.push(r.resolve("untrusted").unwrap()); all } #[test] fn a_reader_client_can_be_shared_across_threads() { fn shared() {} shared::(); shared::(); shared::(); } #[test] fn the_timeouts() { assert_eq!(SEARCH_READ, Duration::from_secs(5)); assert_eq!(CHAT_READ, Duration::from_secs(150)); assert_eq!( ReadTimeouts::DEFAULT, ReadTimeouts { connect: CONNECT, search: SEARCH_READ, chat: CHAT_READ } ); let fake = FakeHoncho::start(); let r = Readers::new(&config(&fake.url), &env, ReadTimeouts::DEFAULT) .unwrap() .0; for reader in all(&r) { // `roncho`'s `Debug` shows the timeouts, and whether there is a key, never the key. let search = format!("{:?}", reader.search()); let chat = format!("{:?}", reader.chat()); for shown in [&search, &chat] { assert!(shown.contains("connect_timeout: 5s"), "{shown}"); } assert!(search.contains("read_timeout: 5s"), "{search}"); assert!(chat.contains("read_timeout: 150s"), "{chat}"); } } #[test] fn each_reader_reads_its_own_workspace_with_its_own_key() { let fake = FakeHoncho::start(); let r = readers(&fake.url); let want = [ ("boxmaker1-trusted", Some(key("read1"))), ("boxmaker2-trusted", None), ("fleet", Some(key("fleet"))), ("boxmaker0-trusted", Some(key("trusted"))), ("boxmaker0-untrusted", None), ]; let readers = all(&r); assert_eq!(readers.len(), want.len()); for (reader, (workspace, _)) in readers.iter().zip(&want) { assert_eq!(reader.search().workspace_id(), *workspace); assert_eq!(reader.chat().workspace_id(), *workspace); // The fake answers a search it has no recording for with 404: only the request matters. let _ = reader .search() .search_workspace(&MessageSearch::new("launch")); let _ = reader .chat() .search_workspace(&MessageSearch::new("launch")); } let requests = fake.requests(); assert_eq!(requests.len(), 2 * want.len()); for (n, request) in requests.iter().enumerate() { let (workspace, secret) = &want[n / 2]; assert_eq!(request.path, format!("/v3/workspaces/{workspace}/search")); let bearer = secret.as_ref().map(|k| format!("Bearer {k}")); assert_eq!( request.header("authorization").map(str::to_string), bearer, "{workspace}" ); } } #[test] fn no_key_is_shown_by_a_reader_or_an_error() { let fake = FakeHoncho::start(); let r = readers(&fake.url); fake.set(Mode::Status(401)); let mut shown = vec![format!("{r:?}")]; for reader in all(&r) { let err = reader .search() .search_workspace(&MessageSearch::new("launch")) .unwrap_err(); let err = classify(&err); shown.push(format!("{err:?}")); shown.push(err.to_string()); } for text in shown { for which in ["read1", "fleet", "trusted"] { assert!(!text.contains(&key(which)), "{text}"); } } // A key that cannot be read stops the build, naming the key and never a value. let mut config = config(&fake.url); config.keys.insert( read_key("boxmaker2"), SecretSource::Env("K_MISSING".to_string()), ); let err = Readers::new(&config, &env, FAST).unwrap_err(); assert!(err.contains("honcho_read_boxmaker2"), "{err}"); assert!( err.contains("see docs/runbook.md#secret-unavailable"), "{err}" ); for which in ["read1", "fleet", "trusted"] { assert!(!err.contains(&key(which)), "{err}"); } } /// `recall` searches the read workspaces, in the order written, then the fleet's; never the /// agent's own two (M5c spec, section 8). #[test] fn recall_never_searches_the_agents_own_workspaces() { let fake = FakeHoncho::start(); let r = readers(&fake.url); let sources: Vec = r .recall_sources() .iter() .map(|s| s.source.clone()) .collect(); assert_eq!( sources, [ Source::Agent("boxmaker1".to_string()), Source::Agent("boxmaker2".to_string()), Source::Fleet ] ); let workspaces: Vec<&str> = r .recall_sources() .iter() .map(|s| s.search().workspace_id()) .collect(); assert_eq!( workspaces, ["boxmaker1-trusted", "boxmaker2-trusted", "fleet"] ); let mut none = config(&fake.url); none.read.clear(); let r = Readers::new(&none, &env, FAST).unwrap().0; let sources: Vec = r .recall_sources() .iter() .map(|s| s.source.clone()) .collect(); assert_eq!(sources, [Source::Fleet]); } fn label(name: &str, trust: Trust, class: DataClass) -> Label { Label { name: name.to_string(), trust, class, } } /// A result is tagged `memory` only from a workspace the owner labelled trusted, or the agent's /// own trusted one; the fleet and everything else is `memory-tainted`. #[test] fn each_source_has_its_label() { let fake = FakeHoncho::start(); let r = readers(&fake.url); let got: Vec<(Label, &str, bool)> = all(&r) .iter() .map(|reader| { ( reader.label.clone(), reader.label.tag(), reader.label.untrusted(), ) }) .collect(); assert_eq!( got, [ ( label("boxmaker1", Trust::Trusted, DataClass::Secret), "memory", false ), ( label("boxmaker2", Trust::Untrusted, DataClass::Public), "memory-tainted", true ), ( label("fleet", Trust::Untrusted, DataClass::Private), "memory-tainted", true ), ( label("own", Trust::Trusted, DataClass::Private), "memory", false ), ( label("untrusted", Trust::Untrusted, DataClass::Private), "memory-tainted", true ), ] ); } #[test] fn ask_memory_names_resolve_to_their_readers() { let fake = FakeHoncho::start(); let r = readers(&fake.url); for (name, source, workspace) in [ ("own", Source::Own, "boxmaker0-trusted"), (UNTRUSTED, Source::OwnUntrusted, "boxmaker0-untrusted"), (FLEET, Source::Fleet, "fleet"), ( "boxmaker1", Source::Agent("boxmaker1".to_string()), "boxmaker1-trusted", ), ( "boxmaker2", Source::Agent("boxmaker2".to_string()), "boxmaker2-trusted", ), ] { let reader = r.resolve(name).unwrap(); assert_eq!(reader.source, source, "{name}"); assert_eq!(reader.chat().workspace_id(), workspace, "{name}"); } for unknown in ["boxmaker9", "boxmaker0", "Own", "boxmaker1-trusted"] { assert_eq!( r.resolve(unknown).unwrap_err(), format!( "no workspace is named {unknown}; ask own, untrusted, fleet, boxmaker1 or boxmaker2" ) ); } let mut none = config(&fake.url); none.read.clear(); let r = Readers::new(&none, &env, FAST).unwrap().0; assert_eq!( r.resolve("boxmaker1").unwrap_err(), "no workspace is named boxmaker1; ask own, untrusted or fleet" ); } /// `own`, `untrusted` and `fleet` name this agent's workspaces in `ask_memory`, so no /// `[[honcho.read]]` agent may be called that: the fleet can never be labelled trusted. #[test] fn no_read_agent_takes_a_reserved_name() { assert_eq!(RESERVED, ["own", "untrusted", "fleet"]); for agent in RESERVED { let section = HonchoSection { url: "http://127.0.0.1:9".to_string(), agent: "boxmaker0".to_string(), owner: "kyle".to_string(), trusted: "boxmaker0-trusted".to_string(), untrusted: "boxmaker0-untrusted".to_string(), fleet: "fleet".to_string(), read: vec![ReadSection { agent: agent.to_string(), workspace: "someone-trusted".to_string(), trust: Some("trusted".to_string()), class: None, }], }; assert_eq!( check(Some(§ion), &BTreeMap::new()).unwrap_err(), format!( "[[honcho.read]] agent {agent:?} is a name ask_memory gives this agent's own \ workspaces: own, untrusted and fleet cannot be agents' names" ) ); } }