memoryd: [honcho] and its keys checked at start, and brokerd's state socket
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
//! `[honcho]` in `memoryd.toml` and its keys in `[secrets.<name>]`, checked at start into a
|
||||
//! `HonchoConfig` (M5c spec, sections 3, 6 and 10). Nothing here reaches Honcho or reads a key: a
|
||||
//! key's source is checked here and read only when a client is made.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use bxcore::DataClass;
|
||||
use bxcore::secrets::{SecretSource, SecretSpec};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// The runbook entry for every reason `check` gives.
|
||||
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 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";
|
||||
pub const UNTRUSTED_KEY: &str = "honcho_untrusted";
|
||||
pub const FLEET_KEY: &str = "honcho_fleet";
|
||||
pub const READ_KEY: &str = "honcho_read_";
|
||||
|
||||
/// `[honcho]` as written. Our own format: unknown keys are errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct HonchoSection {
|
||||
pub url: String,
|
||||
pub agent: String,
|
||||
pub owner: String,
|
||||
pub trusted: String,
|
||||
pub untrusted: String,
|
||||
pub fleet: String,
|
||||
#[serde(default)]
|
||||
pub read: Vec<ReadSection>,
|
||||
}
|
||||
|
||||
/// One `[[honcho.read]]` as written. `trust` and `class` stay text until `check`, so that a wrong
|
||||
/// value is one of its reasons.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ReadSection {
|
||||
pub agent: String,
|
||||
pub workspace: String,
|
||||
#[serde(default)]
|
||||
pub trust: Option<String>,
|
||||
#[serde(default)]
|
||||
pub class: Option<String>,
|
||||
}
|
||||
|
||||
/// `[honcho]` and its keys, checked: every name is a Honcho name, and the names fit together.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HonchoConfig {
|
||||
/// `http://` or `https://`, a host and an optional port.
|
||||
pub url: String,
|
||||
/// This agent's peer.
|
||||
pub agent: String,
|
||||
/// The owner's peer, the same in every workspace of the fleet.
|
||||
pub owner: String,
|
||||
/// This agent's three workspaces.
|
||||
pub trusted: String,
|
||||
pub untrusted: String,
|
||||
pub fleet: String,
|
||||
/// Other agents' workspaces this agent reads, in the order written.
|
||||
pub read: Vec<ReadWorkspace>,
|
||||
/// Every `[secrets.<name>]`, by its name: a source that is checked, never read here.
|
||||
pub keys: BTreeMap<String, SecretSource>,
|
||||
}
|
||||
|
||||
/// One `[[honcho.read]]`, checked.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReadWorkspace {
|
||||
pub agent: String,
|
||||
pub workspace: String,
|
||||
pub trust: Trust,
|
||||
pub class: DataClass,
|
||||
}
|
||||
|
||||
/// How the owner labelled another agent's workspace. `Untrusted` unless written out.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Trust {
|
||||
Trusted,
|
||||
Untrusted,
|
||||
}
|
||||
|
||||
/// The name of the key for the workspace of the read agent `agent`.
|
||||
pub fn read_key(agent: &str) -> String {
|
||||
format!("{READ_KEY}{agent}")
|
||||
}
|
||||
|
||||
/// True when `name` is a Honcho name: 1 to `MAX_NAME` characters, each an ASCII letter, a digit,
|
||||
/// `_` or `-`.
|
||||
pub fn is_name(name: &str) -> bool {
|
||||
!name.is_empty()
|
||||
&& name.len() <= MAX_NAME
|
||||
&& name
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
|
||||
}
|
||||
|
||||
/// Check `[honcho]` and the keys in `[secrets]`. No section and no key is `Ok(None)`: Honcho is
|
||||
/// off. `Err` is the first reason found, one line that names the table and never a key's value.
|
||||
pub fn check(
|
||||
section: Option<&HonchoSection>,
|
||||
secrets: &BTreeMap<String, SecretSpec>,
|
||||
) -> Result<Option<HonchoConfig>, String> {
|
||||
let Some(section) = section else {
|
||||
if let Some(name) = secrets.keys().next() {
|
||||
return Err(format!(
|
||||
"[secrets.{name}] is set, but there is no [honcho] section"
|
||||
));
|
||||
}
|
||||
return Ok(None);
|
||||
};
|
||||
check_url(§ion.url)?;
|
||||
for (key, name) in [
|
||||
("agent", §ion.agent),
|
||||
("owner", §ion.owner),
|
||||
("trusted", §ion.trusted),
|
||||
("untrusted", §ion.untrusted),
|
||||
("fleet", §ion.fleet),
|
||||
] {
|
||||
check_name("[honcho]", key, name)?;
|
||||
}
|
||||
if section.agent == section.owner {
|
||||
return Err(format!(
|
||||
"[honcho] agent and owner are both {:?}: the agent's peer must not be the owner's",
|
||||
section.agent
|
||||
));
|
||||
}
|
||||
check_own_workspaces(section)?;
|
||||
let mut read: Vec<ReadWorkspace> = Vec::new();
|
||||
for entry in §ion.read {
|
||||
let checked = read_workspace(section, entry, &read)?;
|
||||
read.push(checked);
|
||||
}
|
||||
let keys = check_keys(secrets, &read)?;
|
||||
Ok(Some(HonchoConfig {
|
||||
url: section.url.clone(),
|
||||
agent: section.agent.clone(),
|
||||
owner: section.owner.clone(),
|
||||
trusted: section.trusted.clone(),
|
||||
untrusted: section.untrusted.clone(),
|
||||
fleet: section.fleet.clone(),
|
||||
read,
|
||||
keys,
|
||||
}))
|
||||
}
|
||||
|
||||
/// `Err` with the reason when `name`, the value of `key` in `table`, is not a Honcho name.
|
||||
fn check_name(table: &str, key: &str, name: &str) -> Result<(), String> {
|
||||
if is_name(name) {
|
||||
return Ok(());
|
||||
}
|
||||
Err(format!(
|
||||
"{table} {key} {name:?} is not a Honcho name: 1 to {MAX_NAME} of a-z, A-Z, 0-9, _ and -"
|
||||
))
|
||||
}
|
||||
|
||||
/// `http://` or `https://`, then a host, then optionally `:` and a port; nothing else.
|
||||
fn check_url(url: &str) -> Result<(), String> {
|
||||
let rest = if let Some(rest) = url.strip_prefix("https://") {
|
||||
rest
|
||||
} else if let Some(rest) = url.strip_prefix("http://") {
|
||||
rest
|
||||
} else {
|
||||
return Err(URL_REASON.to_string());
|
||||
};
|
||||
let (host, port) = match rest.split_once(':') {
|
||||
Some(pair) => pair,
|
||||
None => {
|
||||
return if host_ok(rest) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(URL_REASON.to_string())
|
||||
};
|
||||
}
|
||||
};
|
||||
if !host_ok(host) {
|
||||
return Err(URL_REASON.to_string());
|
||||
}
|
||||
let port_ok = !port.is_empty()
|
||||
&& port.bytes().all(|b| b.is_ascii_digit())
|
||||
&& port.parse::<u16>().ok().is_some_and(|n| n > 0);
|
||||
if port_ok {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(URL_REASON.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// The host of a URL: not empty, every byte an ASCII letter, digit, `.` or `-`.
|
||||
fn host_ok(host: &str) -> bool {
|
||||
!host.is_empty()
|
||||
&& host
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'-')
|
||||
}
|
||||
|
||||
/// The reason for any `url` that `check_url` refuses. It never quotes the URL, which could hold a
|
||||
/// password (`http://user:password@host`).
|
||||
const URL_REASON: &str =
|
||||
"[honcho] url must be http:// or https://, a host and an optional port, and nothing else";
|
||||
|
||||
/// `Err` when two of this agent's three workspaces have the same name.
|
||||
fn check_own_workspaces(section: &HonchoSection) -> Result<(), String> {
|
||||
let pairs = [
|
||||
(§ion.trusted, §ion.untrusted),
|
||||
(§ion.trusted, §ion.fleet),
|
||||
(§ion.untrusted, §ion.fleet),
|
||||
];
|
||||
for (a, b) in pairs {
|
||||
if a == b {
|
||||
return Err(format!(
|
||||
"[honcho] workspace {a:?} is named twice: trusted, untrusted and fleet must be three different workspaces"
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One `[[honcho.read]]`, checked against `[honcho]` and the entries before it, `earlier`.
|
||||
fn read_workspace(
|
||||
section: &HonchoSection,
|
||||
entry: &ReadSection,
|
||||
earlier: &[ReadWorkspace],
|
||||
) -> Result<ReadWorkspace, String> {
|
||||
check_name("[[honcho.read]]", "agent", &entry.agent)?;
|
||||
check_name("[[honcho.read]]", "workspace", &entry.workspace)?;
|
||||
if entry.agent == section.agent {
|
||||
return Err(format!(
|
||||
"[[honcho.read]] agent {:?} is this agent ([honcho] agent)",
|
||||
entry.agent
|
||||
));
|
||||
}
|
||||
if earlier.iter().any(|r| r.agent == entry.agent) {
|
||||
return Err(format!(
|
||||
"[[honcho.read]] agent {:?} is listed twice",
|
||||
entry.agent
|
||||
));
|
||||
}
|
||||
if entry.workspace == section.trusted
|
||||
|| entry.workspace == section.untrusted
|
||||
|| entry.workspace == section.fleet
|
||||
{
|
||||
return Err(format!(
|
||||
"[[honcho.read]] workspace {:?} is one of this agent's own",
|
||||
entry.workspace
|
||||
));
|
||||
}
|
||||
Ok(ReadWorkspace {
|
||||
agent: entry.agent.clone(),
|
||||
workspace: entry.workspace.clone(),
|
||||
trust: trust_of(entry)?,
|
||||
class: class_of(entry)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// The entry's `trust`: `Untrusted` when it is not written.
|
||||
fn trust_of(entry: &ReadSection) -> Result<Trust, String> {
|
||||
match entry.trust.as_deref() {
|
||||
None | Some("untrusted") => Ok(Trust::Untrusted),
|
||||
Some("trusted") => Ok(Trust::Trusted),
|
||||
Some(other) => Err(format!(
|
||||
"[[honcho.read]] trust {other:?} of agent {:?} is not \"trusted\" or \"untrusted\"",
|
||||
entry.agent
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The entry's `class`: `Private` when it is not written.
|
||||
fn class_of(entry: &ReadSection) -> Result<DataClass, String> {
|
||||
match entry.class.as_deref() {
|
||||
None | Some("private") => Ok(DataClass::Private),
|
||||
Some("public") => Ok(DataClass::Public),
|
||||
Some("secret") => Ok(DataClass::Secret),
|
||||
Some(other) => Err(format!(
|
||||
"[[honcho.read]] class {other:?} of agent {:?} is not \"public\", \"private\" or \"secret\"",
|
||||
entry.agent
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every `[secrets.<name>]`: a Honcho key's name, and a source that `SecretSpec::source` accepts.
|
||||
fn check_keys(
|
||||
secrets: &BTreeMap<String, SecretSpec>,
|
||||
read: &[ReadWorkspace],
|
||||
) -> Result<BTreeMap<String, SecretSource>, String> {
|
||||
let mut keys: BTreeMap<String, SecretSource> = BTreeMap::new();
|
||||
for (name, spec) in secrets {
|
||||
let is_read_key = matches!(name.strip_prefix(READ_KEY), Some(agent) if read.iter().any(|r| r.agent == agent));
|
||||
if !is_read_key && name != TRUSTED_KEY && name != UNTRUSTED_KEY && name != FLEET_KEY {
|
||||
if let Some(agent) = name.strip_prefix(READ_KEY) {
|
||||
return Err(format!(
|
||||
"[secrets.{name}] is the key of agent {agent:?}, which is not in [[honcho.read]]"
|
||||
));
|
||||
}
|
||||
return Err(format!(
|
||||
"[secrets.{name}] is not a Honcho key: memoryd has only honcho_trusted, honcho_untrusted, honcho_fleet and honcho_read_<agent>"
|
||||
));
|
||||
}
|
||||
let source = spec
|
||||
.source()
|
||||
.map_err(|why| format!("[secrets.{name}] {why}"))?;
|
||||
keys.insert(name.clone(), source);
|
||||
}
|
||||
Ok(keys)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
//! Honcho, the memory this agent shares with the fleet, reached through `roncho` (M5c spec).
|
||||
|
||||
pub mod config;
|
||||
Reference in New Issue
Block a user