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:
@@ -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(§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> {
|
||||
// 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!()
|
||||
}
|
||||
Reference in New Issue
Block a user