368 lines
12 KiB
Rust
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(§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"
|
|
)
|
|
);
|
|
}
|
|
}
|