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}");
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ reviewer adds findings under "Reviews" once per milestone.
|
||||
|
||||
| Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| M5c2/05-memoryd-honcho-readers | 2026-09-29 | done | 1 | pass | none | Copied the given test `crates/memoryd/tests/honcho_read.rs` (new), new versions of `crates/memoryd/tests/honcho_client.rs` and `honcho_start.rs` (they replace the tree's), and the new source `crates/memoryd/src/honcho/read.rs` (complete, no `todo!`). Step 2 (`cargo build --tests --keep-going -p memoryd`) showed exactly the two named failures: `honcho_read` with one E0432 `unresolved import memoryd::honcho::config::RESERVED` and one E0432 `unresolved import memoryd::honcho::read`; every other target compiled. `cargo test --no-fail-fast` on `honcho_client` and `honcho_start` failed the two named tests: `the_timeouts` (`CHAT_READ` is 180 s) and `a_read_key_that_cannot_be_read_stops_memoryd` (memoryd starts instead of stopping). Made the edits exactly as the task gives: H1 (`pub mod read;` after `pub mod naming;` in `honcho/mod.rs`); C1 (`CHAT_READ` = 150 s, doc now `M5c spec, section 9`); C2 (`client` `pub(crate)`); C3 (`key` `pub(crate)`); G1 (`RESERVED` `["own", "untrusted", "fleet"]` before the key names, doc naming section 9); G2 (the reserved-names check in `read_workspace` after the this-agent check, its sentence split with `\` as shown); O1 (`use ...read::{ReadTimeouts, Readers}`); O2 (`Opened::readers` `Arc<Readers>` after `sync`, before `clients`); O3 (`Readers::new` beside `Clients::new`, `StartError::Secret`, the readers' warnings filtered to those not among the writers' then logged once); D1 (the reserved names among the `honcho-config-invalid` reasons in `docs/runbook.md`). `cargo check -p memoryd` clean, no warning. `cargo test` passes: 8 in `honcho_read`, 13 in `honcho_client`, 8 in `honcho_config`, 3 in `honcho_keys` (1 ignored), 5 in `honcho_start`. `git diff --stat -- crates/memoryd/src` lists only `client.rs`, `config.rs`, `mod.rs` and `start.rs`; `git diff --stat -- crates/memoryd/src/serve.rs crates/memoryd/src/recall.rs crates/memoryd/src/worker.rs Cargo.toml Cargo.lock` prints nothing. `make gate` printed `gate: ok` with 1403 tests (1402 in `cargo test --workspace`, plus the 1 ignored `end_to_end` the gate runs separately; the +9 over the 1394 before is exactly the 8 `honcho_read` tests and the 1 new `honcho_start` test). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
|
||||
| M5c2/04-loopd-ask-memory | 2026-09-28 | done | 1 | pass | none | Copied the three given tests from `docs/plans/M5c2/files/crates/loopd/tests/`: `ask_memory_tool.rs` (new) and `broker_port_ask_wait.rs` (new), and a new version of `device.rs` (replaces the tree's). Step 2 (`cargo build --tests --keep-going -p loopd`) showed exactly the 14 named failures: `ask_memory_tool` with one E0432 `unresolved imports loopd::memory::ASK_MEMORY`, `loopd::memory::ask_memory_schema` and three E0599 for `m5c` on `Registry`; `broker_port_ask_wait` with one E0432 `unresolved import loopd::broker_port::ASK_MEMORY_WAIT`, five E0599 for `timeout_for` and three for `with_ask_wait`; `device` with one E0599 for `m5c`; every other target compiled. Made the edits exactly as the task gives: S1 (`ASK_MEMORY` = `bxcore::memory::ASK_MEMORY`), S2 (`ask_memory_schema` with `maxItems: 2`, description one string split with `\` as shown); T1 (`m5a()` comment drops `loopd serve`); T2 (`m5c()` inserts `ask_memory` core at index 3 after `m5a()`); N1 (`main.rs` uses `Registry::m5c()`); Y1 (`config/system.md` last sentence names `ask_memory`); P1 (`ASK_MEMORY_WAIT` 180 s before `POINTER`), P2 (`BrokerPort` gains `ask_timeout` after `timeout`), P3 (`new` sets `ask_timeout: ASK_MEMORY_WAIT`, adds `with_ask_wait` via struct update and `timeout_for` matching `ASK_MEMORY` -> `timeout.max(ask_timeout)` else `timeout`), P4 (`with_log` sets `ask_timeout: ASK_MEMORY_WAIT`), P5 (`call` binds `timeout = self.timeout_for(&request.tool)`, uses it for the first deadline), P6 (write timeout), P7 (the pending-frame extension). `cargo check -p loopd` clean, and `grep -n 'self.timeout'` prints the three expected lines. `cargo test` passes: 6 in `ask_memory_tool`, 4 in `broker_port_ask_wait`, 15 in `broker_port`, 2 in `broker_port_bad`, 2 in `broker_port_cap`, 4 in `memory`, 6 in `turn_broker`. `git diff --stat` lists only the five edited files and `device.rs`; `git diff config/system.md | grep -c '^[-+]You are'` prints 2. `make gate` printed `gate: ok` with 1394 tests (1393 in `cargo test --workspace`, plus the 1 ignored `end_to_end`; the +10 over the 1384 before is exactly the 6 in `ask_memory_tool.rs` and 4 in `broker_port_ask_wait.rs`). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
|
||||
| M5c2/03-brokerd-ask-memory-wait | 2026-09-28 | done | 1 | pass | none | Copied the given test `crates/brokerd/tests/memory_wait.rs` from `docs/plans/M5c2/files/crates/brokerd/tests/`. Step 2 (`cargo build --tests --keep-going -p brokerd`) showed exactly the 10 named failures: one E0432 `unresolved import brokerd::memory_port::ASK_MEMORY_WAIT`, six E0599 for `wait_for`, three E0599 for `with_ask_wait`; every other target compiled. Made edits W1 (module doc naming `ask_memory` and the `ask_memory` wait of its own), W2 (`use crate::args::ToolName`), W3 (`MEMORY_WAIT` doc now says `recall` or `memory`; added `ASK_MEMORY_WAIT` 170 s after it with no blank line between, `MemoryPort` gains `ask_wait` last, `new` sets `ask_wait: ASK_MEMORY_WAIT`, `with_ask_wait` via struct update, `wait_for` matches `AskMemory` -> `ask_wait` else `wait`), W4 (ask comment `self.wait_for(spec.tool())`), W5 (`ask` binds `wait = self.wait_for(spec.tool())` and uses it for both timeouts). `cargo check -p brokerd` clean. `cargo test -p brokerd --test memory_wait --test memory_port --test ask_memory_tool` passes: 3 in `memory_wait`, 5 in `memory_port`, 9 in `ask_memory_tool`. `git diff --stat` lists only `memory_port.rs`; `grep -n 'MEMORY_WAIT' crates/brokerd/src/main.rs` prints the `use` line and `MemoryPort::new(cfg.memory_socket(), MEMORY_WAIT)`. `make gate` printed `gate: ok` with 1384 tests (1383 in `cargo test --workspace`, plus the 1 ignored `end_to_end`; the +3 over the 1381 before is exactly the 3 new `memory_wait` tests). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
|
||||
| M5c2/02-brokerd-ask-memory | 2026-09-28 | done | 1 | pass | none | Copied the given tests from `docs/plans/M5c2/files/crates/brokerd/tests/`: `ask_memory_tool.rs` (new) and new versions of `grants.rs` and `memory_tools.rs`. Step 2 (`cargo build --tests --keep-going -p brokerd`) showed exactly the 11 named failures: one E0432 `unresolved import brokerd::builtin::ASK_MEMORY`, one E0599 for `AskMemory` on `ToolArgs`, nine E0599 for `AskMemory` on `ToolName`; every other target compiled. `cargo test --no-fail-fast` on `grants` and `memory_tools` failed the two named lines (`an_empty_directory_is_a_valid_empty_set`, `grant_files_cannot_name_the_memory_tools_or_take_a_built_in_id`). Made edits A1 (`use bxcore::AskMemoryArgs`), A2 (`ToolName::AskMemory`), A3 (`ask_memory` in `parse`), A4 (`ask_memory` in `as_str`), A5 (`is_memory`), A6 (`ToolArgs::AskMemory`), A7 (`tool()`), A8 (`canonical_json`), A9 (`parse` decodes then runs `check`); B1 (module doc naming the three tools), B2 (`ASK_MEMORY` grant text, `BUILT_IN` sentence naming the three, `sources` with `builtin-ask-memory` last), C1 (`time_limit`), P1 (`matching` takes no constraints), R1 (`runner` no mounts/egress). `cargo check -p brokerd` clean. `cargo test` passes: 9 in `ask_memory_tool`, 17 in `grants`, 4 in `memory_tools`, 5 in `memory_port`, 8 in `runner`. `git diff --stat -- crates/brokerd/src` lists only the five files, not `grants.rs`/`container.rs`/`memory_port.rs`; the other-crates diff is empty. `make gate` printed `gate: ok` with 1381 tests (1380 in `cargo test --workspace`, plus the 1 ignored `end_to_end`; the +9 over the 1372 before is exactly the 9 new `ask_memory_tool` tests). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
|
||||
|
||||
+4
-3
@@ -916,9 +916,10 @@ they do not belong in, so `memoryd` checks the section before it starts. The rea
|
||||
or a space is not allowed.
|
||||
- `agent` and `owner` are the same peer, or two of `trusted`, `untrusted` and `fleet` are the same
|
||||
workspace.
|
||||
- A `[[honcho.read]]` names this agent, names an agent twice, or names one of this agent's three
|
||||
workspaces; or its `trust` is not `trusted` or `untrusted`, or its `class` not `public`,
|
||||
`private` or `secret` (all lower case).
|
||||
- A `[[honcho.read]]` names this agent, names an agent twice, names an agent `own`, `untrusted`
|
||||
or `fleet` (the names `ask_memory` gives this agent's own workspaces), or names one of this
|
||||
agent's three workspaces; or its `trust` is not `trusted` or `untrusted`, or its `class` not
|
||||
`public`, `private` or `secret` (all lower case).
|
||||
- A `[secrets.<name>]` is not `honcho_trusted`, `honcho_untrusted`, `honcho_fleet` or
|
||||
`honcho_read_<agent>` for an agent in `[[honcho.read]]`; or it does not name exactly one
|
||||
well-formed `credential`, `env` or `file` (as in `secret-unavailable`); or there is no
|
||||
|
||||
Reference in New Issue
Block a user