M5c-1 plan: task 06, memoryd checks [honcho] and its keys at start

memoryd.toml gains an optional [honcho] section, [[honcho.read]] and the
Honcho keys in [secrets.<name>], checked at start into
memoryd::honcho::config::HonchoConfig (13 reasons, runbook entry
honcho-config-invalid given as a fragment), and [sockets] state for
brokerd's state.sock. Keys are checked, never read; no [honcho] leaves
memoryd as it was. memoryd enables bxcore's feature secrets.

Checks: a reference at ref 05 + this task passes make gate with 1188
tests (1171 + 17); /tmp unchanged around the gate; the given tests fail
before as the task says (two targets do not compile, 4 start tests
fail); 49 one-line mutations of the reference, all caught (one gap, a
URL with a user and no password, found and closed); the new tests 40
times under CPU load, no failure; a replay from the task text on a fresh
ref-05 worktree with its own target is byte-identical and passes the
gate, and every before block was found once; given files pass fmt and
the 500-line limit; no chat-template tag in any file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 02:48:16 -07:00
co-authored by Claude Opus 5.5
parent ba8267c7dc
commit 0e1e070fc3
6 changed files with 1686 additions and 0 deletions
@@ -0,0 +1,220 @@
//! `[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 {
// Not empty, `name.len() <= MAX_NAME`, and every byte of `name.bytes()` is
// `is_ascii_alphanumeric()`, `b'_'` or `b'-'`. Bytes, not chars: a non-ASCII letter is refused.
todo!()
}
/// 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(&section.url)?;
for (key, name) in [
("agent", &section.agent),
("owner", &section.owner),
("trusted", &section.trusted),
("untrusted", &section.untrusted),
("fleet", &section.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 &section.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> {
// 1. `url.strip_prefix("https://")`, else `url.strip_prefix("http://")`, else the error below.
// 2. What is left, `split_once(':')`: the host before the first `:` and the port after it.
// 3. The host: not empty, every byte `is_ascii_alphanumeric()`, `b'.'` or `b'-'`.
// 4. A port, if there is a `:`: not empty, every byte `is_ascii_digit()`, and
// `port.parse::<u16>()` is `Ok(n)` with `n > 0`.
// Both good: `Ok(())`. Anything else: `Err(URL_REASON.to_string())`.
todo!()
}
/// 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> {
// The pairs (trusted, untrusted), (trusted, fleet), (untrusted, fleet), in that order: the first
// pair whose two names are equal is `Err` with reason R5 and that name. None equal: `Ok(())`.
todo!()
}
/// One `[[honcho.read]]`, checked against `[honcho]` and the entries before it, `earlier`.
fn read_workspace(
section: &HonchoSection,
entry: &ReadSection,
earlier: &[ReadWorkspace],
) -> Result<ReadWorkspace, String> {
// In this order, `Err` at the first that fails: `check_name("[[honcho.read]]", "agent", ..)`,
// then the same for "workspace" (R2); the agent is `section.agent` (R6); an entry of `earlier`
// has the same agent (R7); the workspace is `section.trusted`, `.untrusted` or `.fleet` (R8).
// Then `Ok(ReadWorkspace { .. })`: both names cloned, `trust: trust_of(entry)?` and
// `class: class_of(entry)?`.
todo!()
}
/// The entry's `trust`: `Untrusted` when it is not written.
fn trust_of(entry: &ReadSection) -> Result<Trust, String> {
// `match entry.trust.as_deref()`: `None` or `Some("untrusted")` is `Trust::Untrusted`,
// `Some("trusted")` is `Trust::Trusted`, and any other text (case counts) is reason R9.
todo!()
}
/// The entry's `class`: `Private` when it is not written.
fn class_of(entry: &ReadSection) -> Result<DataClass, String> {
// `match entry.class.as_deref()`: `None` or `Some("private")` is `DataClass::Private`,
// `"public"` and `"secret"` are `Public` and `Secret`, any other text (case counts) is R10.
todo!()
}
/// 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> {
// For each `(name, spec)` of `secrets`, in the map's order: a name other than `TRUSTED_KEY`,
// `UNTRUSTED_KEY` and `FLEET_KEY` must be `READ_KEY` and the agent of an entry of `read`
// (`name.strip_prefix(READ_KEY)`); with the prefix and any other agent it is R11, without the
// prefix R12. Then `spec.source()`: `Err(why)` is R13; `Ok(source)` goes into a new
// `BTreeMap` under `name.clone()`. Return the map.
todo!()
}