Files

368 lines
12 KiB
Rust

//! 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<OsString> {
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<T: Send + Sync>() {}
shared::<roncho::blocking::Client>();
shared::<Reader>();
shared::<Readers>();
}
#[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<Source> = 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<Source> = 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(&section), &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"
)
);
}
}