memoryd: the Honcho workspaces it reads, with their clients and labels
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -19,8 +19,8 @@ use crate::honcho::naming::HonchoSession;
|
||||
/// How long a connection may take, and a read may go without a byte.
|
||||
pub const CONNECT: Duration = Duration::from_secs(5);
|
||||
pub const READ: Duration = Duration::from_secs(30);
|
||||
/// A chat's read: a non-streamed chat sends nothing until it is done (M5c-2's `ask_memory`).
|
||||
pub const CHAT_READ: Duration = Duration::from_secs(180);
|
||||
/// A chat's read: a non-streamed chat sends nothing until it is done (M5c spec, section 9).
|
||||
pub const CHAT_READ: Duration = Duration::from_secs(150);
|
||||
|
||||
/// The timeouts a client is built with. Tests shorten them.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -176,7 +176,7 @@ fn created<T>(result: Result<T, Error>) -> Result<(), HonchoError> {
|
||||
}
|
||||
|
||||
/// One workspace's client: no `roncho.toml` read, the timeouts given, the key when there is one.
|
||||
fn client(
|
||||
pub(crate) fn client(
|
||||
url: &str,
|
||||
workspace: &str,
|
||||
key: Option<&secrets::Secret>,
|
||||
@@ -197,7 +197,7 @@ fn client(
|
||||
}
|
||||
|
||||
/// The key named `name` in `[secrets]`, if one is configured, read now.
|
||||
fn key(
|
||||
pub(crate) fn key(
|
||||
config: &HonchoConfig,
|
||||
name: &str,
|
||||
env: &dyn Fn(&str) -> Option<OsString>,
|
||||
|
||||
@@ -14,6 +14,10 @@ pub const RUNBOOK: &str = "see docs/runbook.md#honcho-config-invalid";
|
||||
/// The longest name Honcho accepts for a workspace, a peer or a session (M5c spec, section 6).
|
||||
pub const MAX_NAME: usize = 512;
|
||||
|
||||
/// The names `ask_memory` gives this agent's own workspaces and the fleet's (M5c spec, section 9),
|
||||
/// which no `[[honcho.read]]` agent may have.
|
||||
pub const RESERVED: [&str; 3] = ["own", "untrusted", "fleet"];
|
||||
|
||||
/// The names of the keys in `[secrets.<name>]`, one per workspace. A read workspace's key is
|
||||
/// `READ_KEY` and its agent's name (`read_key`).
|
||||
pub const TRUSTED_KEY: &str = "honcho_trusted";
|
||||
@@ -233,6 +237,13 @@ fn read_workspace(
|
||||
entry.agent
|
||||
));
|
||||
}
|
||||
if RESERVED.contains(&entry.agent.as_str()) {
|
||||
return Err(format!(
|
||||
"[[honcho.read]] agent {:?} is a name ask_memory gives this agent's own workspaces: \
|
||||
own, untrusted and fleet cannot be agents' names",
|
||||
entry.agent
|
||||
));
|
||||
}
|
||||
if earlier.iter().any(|r| r.agent == entry.agent) {
|
||||
return Err(format!(
|
||||
"[[honcho.read]] agent {:?} is listed twice",
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod ledger;
|
||||
pub mod messages;
|
||||
pub mod mirror;
|
||||
pub mod naming;
|
||||
pub mod read;
|
||||
pub mod report;
|
||||
pub mod start;
|
||||
pub mod state;
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
//! The Honcho workspaces this agent reads (M5c spec, sections 3, 8 and 9): each `[[honcho.read]]`
|
||||
//! workspace, the fleet's, and the agent's own two, which only `ask_memory` asks. Each has a
|
||||
//! client for searches and one for chat, whose answers take far longer, and the label its results
|
||||
//! carry. Nothing here writes to Honcho, keeps a response or shows a key.
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::time::Duration;
|
||||
|
||||
use bxcore::DataClass;
|
||||
use bxcore::memory::OWN_WORKSPACE;
|
||||
use roncho::blocking::Client;
|
||||
|
||||
use crate::honcho::client::{CHAT_READ, CONNECT, Timeouts, client, key};
|
||||
use crate::honcho::config::{FLEET_KEY, HonchoConfig, TRUSTED_KEY, Trust, UNTRUSTED_KEY, read_key};
|
||||
|
||||
/// How long a search may go without a byte (M5c spec, section 8).
|
||||
pub const SEARCH_READ: Duration = Duration::from_secs(5);
|
||||
/// The names `ask_memory` gives the agent's own untrusted workspace and the fleet's. The own
|
||||
/// trusted one is `bxcore::memory::OWN_WORKSPACE`, `own`.
|
||||
pub const UNTRUSTED: &str = "untrusted";
|
||||
pub const FLEET: &str = "fleet";
|
||||
|
||||
/// The timeouts the read clients are built with. Tests shorten them.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ReadTimeouts {
|
||||
pub connect: Duration,
|
||||
pub search: Duration,
|
||||
pub chat: Duration,
|
||||
}
|
||||
|
||||
impl ReadTimeouts {
|
||||
pub const DEFAULT: ReadTimeouts = ReadTimeouts {
|
||||
connect: CONNECT,
|
||||
search: SEARCH_READ,
|
||||
chat: CHAT_READ,
|
||||
};
|
||||
}
|
||||
|
||||
/// What a reader reads.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Source {
|
||||
/// Another agent's workspace, from `[[honcho.read]]`, by the agent's name.
|
||||
Agent(String),
|
||||
Fleet,
|
||||
/// This agent's own trusted workspace, and its own untrusted one: `ask_memory` only.
|
||||
Own,
|
||||
OwnUntrusted,
|
||||
}
|
||||
|
||||
/// What a reader's results are labelled with: the name each hit shows, whether the owner trusts
|
||||
/// the workspace, and its class.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Label {
|
||||
pub name: String,
|
||||
pub trust: Trust,
|
||||
pub class: DataClass,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
/// The tag on each hit, as a local `recall` hit has: `memory` when trusted, else
|
||||
/// `memory-tainted`.
|
||||
pub fn tag(&self) -> &'static str {
|
||||
match self.trust {
|
||||
Trust::Trusted => "memory",
|
||||
Trust::Untrusted => "memory-tainted",
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a result makes the session untrusted.
|
||||
pub fn untrusted(&self) -> bool {
|
||||
self.trust == Trust::Untrusted
|
||||
}
|
||||
}
|
||||
|
||||
/// One workspace this agent reads: what it is, its label, and its two clients.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Reader {
|
||||
pub source: Source,
|
||||
pub label: Label,
|
||||
search: Client,
|
||||
chat: Client,
|
||||
}
|
||||
|
||||
impl Reader {
|
||||
/// The client for searches and conclusion queries, with the short read timeout.
|
||||
pub fn search(&self) -> &Client {
|
||||
&self.search
|
||||
}
|
||||
|
||||
/// The client for workspace chat, with the long read timeout.
|
||||
pub fn chat(&self) -> &Client {
|
||||
&self.chat
|
||||
}
|
||||
}
|
||||
|
||||
/// Every workspace this agent reads, built at start with `[honcho]`. Shared by the threads that
|
||||
/// search and ask (`roncho`'s blocking client is `Send + Sync`).
|
||||
#[derive(Debug)]
|
||||
pub struct Readers {
|
||||
read: Vec<Reader>,
|
||||
fleet: Reader,
|
||||
own: Reader,
|
||||
own_untrusted: Reader,
|
||||
}
|
||||
|
||||
/// What every reader is built from, and the key files' warnings so far.
|
||||
struct Build<'a> {
|
||||
config: &'a HonchoConfig,
|
||||
env: &'a dyn Fn(&str) -> Option<OsString>,
|
||||
timeouts: ReadTimeouts,
|
||||
warnings: Vec<String>,
|
||||
}
|
||||
|
||||
impl Build<'_> {
|
||||
/// A reader of `workspace`: both clients, with the key named `key_name` when one is
|
||||
/// configured, read once.
|
||||
fn reader(
|
||||
&mut self,
|
||||
workspace: &str,
|
||||
key_name: &str,
|
||||
source: Source,
|
||||
label: Label,
|
||||
) -> Result<Reader, String> {
|
||||
let secret = key(self.config, key_name, self.env, &mut self.warnings)?;
|
||||
let search = Timeouts {
|
||||
connect: self.timeouts.connect,
|
||||
read: self.timeouts.search,
|
||||
};
|
||||
let chat = Timeouts {
|
||||
connect: self.timeouts.connect,
|
||||
read: self.timeouts.chat,
|
||||
};
|
||||
let url = &self.config.url;
|
||||
Ok(Reader {
|
||||
source,
|
||||
label,
|
||||
search: client(url, workspace, secret.as_ref(), search)?,
|
||||
chat: client(url, workspace, secret.as_ref(), chat)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A `private` label.
|
||||
fn private(name: &str, trust: Trust) -> Label {
|
||||
Label {
|
||||
name: name.to_string(),
|
||||
trust,
|
||||
class: DataClass::Private,
|
||||
}
|
||||
}
|
||||
|
||||
impl Readers {
|
||||
/// A reader for each `[[honcho.read]]` workspace, in the order written, then the fleet's and
|
||||
/// the agent's own two. The warnings are the key files' plaintext warnings, for the caller to
|
||||
/// log. An error names the key or the workspace, never a key's value.
|
||||
pub fn new(
|
||||
config: &HonchoConfig,
|
||||
env: &dyn Fn(&str) -> Option<OsString>,
|
||||
timeouts: ReadTimeouts,
|
||||
) -> Result<(Readers, Vec<String>), String> {
|
||||
let mut build = Build {
|
||||
config,
|
||||
env,
|
||||
timeouts,
|
||||
warnings: Vec::new(),
|
||||
};
|
||||
let mut read = Vec::new();
|
||||
for entry in &config.read {
|
||||
let label = Label {
|
||||
name: entry.agent.clone(),
|
||||
trust: entry.trust,
|
||||
class: entry.class,
|
||||
};
|
||||
let source = Source::Agent(entry.agent.clone());
|
||||
read.push(build.reader(&entry.workspace, &read_key(&entry.agent), source, label)?);
|
||||
}
|
||||
let fleet = private(FLEET, Trust::Untrusted);
|
||||
let fleet = build.reader(&config.fleet, FLEET_KEY, Source::Fleet, fleet)?;
|
||||
let own = private(OWN_WORKSPACE, Trust::Trusted);
|
||||
let own = build.reader(&config.trusted, TRUSTED_KEY, Source::Own, own)?;
|
||||
let own_untrusted = private(UNTRUSTED, Trust::Untrusted);
|
||||
let own_untrusted = build.reader(
|
||||
&config.untrusted,
|
||||
UNTRUSTED_KEY,
|
||||
Source::OwnUntrusted,
|
||||
own_untrusted,
|
||||
)?;
|
||||
let readers = Readers {
|
||||
read,
|
||||
fleet,
|
||||
own,
|
||||
own_untrusted,
|
||||
};
|
||||
Ok((readers, build.warnings))
|
||||
}
|
||||
|
||||
/// What `recall` searches: the `[[honcho.read]]` workspaces in the order written, then the
|
||||
/// fleet's. Never the agent's own (M5c spec, section 8): the local index holds their text.
|
||||
pub fn recall_sources(&self) -> Vec<&Reader> {
|
||||
self.read
|
||||
.iter()
|
||||
.chain(std::iter::once(&self.fleet))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The reader `ask_memory` names `name`: `own`, `untrusted`, `fleet`, or a `[[honcho.read]]`
|
||||
/// agent. Any other name is the sentence the model is shown, listing the names it may use.
|
||||
pub fn resolve(&self, name: &str) -> Result<&Reader, String> {
|
||||
match name {
|
||||
OWN_WORKSPACE => return Ok(&self.own),
|
||||
UNTRUSTED => return Ok(&self.own_untrusted),
|
||||
FLEET => return Ok(&self.fleet),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(reader) = self.read.iter().find(|r| r.label.name == name) {
|
||||
return Ok(reader);
|
||||
}
|
||||
let mut names = vec![OWN_WORKSPACE, UNTRUSTED, FLEET];
|
||||
names.extend(self.read.iter().map(|r| r.label.name.as_str()));
|
||||
let (last, rest) = names.split_last().unwrap_or((&FLEET, &[]));
|
||||
Err(format!(
|
||||
"no workspace is named {name}; ask {} or {last}",
|
||||
rest.join(", ")
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use crate::honcho::client::{Clients, Timeouts};
|
||||
use crate::honcho::config::HonchoConfig;
|
||||
use crate::honcho::ledger::{Ledger, Record};
|
||||
use crate::honcho::mirror::Mirror;
|
||||
use crate::honcho::read::{ReadTimeouts, Readers};
|
||||
use crate::honcho::state;
|
||||
use crate::honcho::turns::{LEDGER, TurnSync};
|
||||
use crate::honcho::worker::{AskBox, Tuning, Wake, Worker};
|
||||
@@ -19,6 +20,8 @@ use crate::serve::{Log, StartError};
|
||||
/// clients with their keys read. Nothing is bound or started yet.
|
||||
pub struct Opened {
|
||||
pub sync: Arc<Mutex<TurnSync>>,
|
||||
/// The workspaces `recall` and `ask_memory` read (M5c spec, sections 8 and 9).
|
||||
pub readers: Arc<Readers>,
|
||||
clients: Clients,
|
||||
agent: String,
|
||||
owner: String,
|
||||
@@ -65,11 +68,19 @@ pub fn open(config: &Config, honcho: &HonchoConfig, log: &Log) -> Result<Opened,
|
||||
let env = |name: &str| std::env::var_os(name);
|
||||
let (clients, warnings) =
|
||||
Clients::new(honcho, &env, Timeouts::WRITES).map_err(StartError::Secret)?;
|
||||
for warning in warnings {
|
||||
let (readers, read_warnings) =
|
||||
Readers::new(honcho, &env, ReadTimeouts::DEFAULT).map_err(StartError::Secret)?;
|
||||
// The readers load the keys of the three written workspaces again: each warning once.
|
||||
let read_warnings: Vec<String> = read_warnings
|
||||
.into_iter()
|
||||
.filter(|w| !warnings.contains(w))
|
||||
.collect();
|
||||
for warning in warnings.into_iter().chain(read_warnings) {
|
||||
(log)(&warning);
|
||||
}
|
||||
Ok(Opened {
|
||||
sync: Arc::new(Mutex::new(sync)),
|
||||
readers: Arc::new(readers),
|
||||
clients,
|
||||
agent: honcho.agent.clone(),
|
||||
owner: honcho.owner.clone(),
|
||||
|
||||
@@ -74,7 +74,7 @@ fn post(path: &str, body: Value) -> (String, String, Value) {
|
||||
fn the_timeouts() {
|
||||
assert_eq!(CONNECT, Duration::from_secs(5));
|
||||
assert_eq!(READ, Duration::from_secs(30));
|
||||
assert_eq!(CHAT_READ, Duration::from_secs(180));
|
||||
assert_eq!(CHAT_READ, Duration::from_secs(150));
|
||||
assert_eq!(
|
||||
Timeouts::WRITES,
|
||||
Timeouts {
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
//! 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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,22 @@ fn a_key_for_an_agent_not_read_stops_memoryd() {
|
||||
assert_no_key(&stderr);
|
||||
}
|
||||
|
||||
/// The readers are built at start (M5c-2): a read workspace's key that cannot be read stops
|
||||
/// `memoryd`, as a written workspace's does.
|
||||
#[test]
|
||||
fn a_read_key_that_cannot_be_read_stops_memoryd() {
|
||||
let extra = format!("{SECTION}[secrets.honcho_read_boxmaker1]\nenv = \"HONCHO_NOT_SET\"\n");
|
||||
let home = Home::new("honcho-read-key", &extra);
|
||||
let (code, stderr) = home.run_to_end();
|
||||
assert_eq!(code, Some(1), "{stderr}");
|
||||
assert!(stderr.contains("honcho_read_boxmaker1"), "{stderr}");
|
||||
assert!(
|
||||
stderr.contains("see docs/runbook.md#secret-unavailable"),
|
||||
"{stderr}"
|
||||
);
|
||||
assert_no_key(&stderr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_key_in_the_section_is_still_a_start_failure() {
|
||||
let extra = SECTION.replace("fleet = \"fleet\"", "fleet = \"fleet\"\nfleat = \"x\"");
|
||||
@@ -234,5 +250,13 @@ fn a_valid_section_with_keys_starts_memoryd_as_before() {
|
||||
let stderr = home.stderr();
|
||||
assert!(stderr.contains("memoryd: serving\n"), "{stderr}");
|
||||
assert_no_key(&stderr);
|
||||
// The writers and the readers both load the fleet's key file; its warning is logged once.
|
||||
assert_eq!(
|
||||
stderr
|
||||
.matches("secret honcho_fleet is read in plaintext")
|
||||
.count(),
|
||||
1,
|
||||
"{stderr}"
|
||||
);
|
||||
assert!(!stderr.contains(POINTER), "{stderr}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user