# M5c-1 task 06: `memoryd` checks its `[honcho]` section and its keys at start **Branch:** `m5c1` (run `git switch m5c1`; `git status --short` must be empty, otherwise stop) **Commit subject:** `memoryd: [honcho] and its keys checked at start, and brokerd's state socket` **Log row:** the Task cell is `M5c1/06-memoryd-honcho-config`. ## Goal From M5c on, `memoryd` sends finished turns and memory writes to Honcho, the memory the agents of the fleet share (M5c spec, sections 3 and 10). This task adds the configuration for it, read and checked when `memoryd` starts. Nothing talks to Honcho or to `brokerd` yet: later tasks use what this task checks. - **`[honcho]`**, optional, in `memoryd.toml`: ```toml [honcho] url = "http://rift.scylla-hammerhead.ts.net:8000" # http(s)://, a host, optional port agent = "boxmaker0" # this agent's peer owner = "kyle" # the owner's peer, the same in the fleet trusted = "boxmaker0-trusted" # this agent's three workspaces untrusted = "boxmaker0-untrusted" fleet = "fleet" [[honcho.read]] # zero or more: other agents' workspaces agent = "boxmaker1" workspace = "boxmaker1-trusted" trust = "untrusted" # "trusted" or "untrusted"; the default class = "private" # "public", "private" or "secret"; default [secrets.honcho_trusted] # optional: a key per workspace env = "HONCHO_KEY" ``` The keys' names are `honcho_trusted`, `honcho_untrusted`, `honcho_fleet`, and `honcho_read_` for an agent in `[[honcho.read]]`. A key's `[secrets.]` table is the one every role uses (`credential`, `env` or `file`, M5b). Here a key is only **checked** (its table names one well-formed source); it is **not read**: a later task reads it when it makes the Honcho client. No key is ever printed. - **No `[honcho]` section** (and no key): Honcho is off, and `memoryd` behaves exactly as before. Every existing `memoryd` test passes unchanged. - **An invalid section** stops `memoryd` at start, before it takes its lock or touches its home, with one line and the pointer: ```text memoryd: /etc/boxmaker/memoryd.toml: [honcho] agent and owner are both "kyle": the agent's peer must not be the owner's see docs/runbook.md#honcho-config-invalid ``` The reasons are in the table "The reasons" below. This task appends the runbook entry, given. - **`[sockets] state`**: the path of `brokerd`'s `state.sock` (task 05), which `memoryd` will connect to, to ask for a session's state. Like the other three `[sockets]` keys, empty or missing means the default under `home`: `/run/memory-broker/state.sock`, the path `brokerd` serves by default. ## Interfaces Later tasks use these exactly as written. They are in the skeleton you copy (step 3); only the bodies of seven private or small functions are yours to fill. In `crates/memoryd/src/honcho/config.rs` (the module `memoryd::honcho::config`): ```rust pub const RUNBOOK: &str = "see docs/runbook.md#honcho-config-invalid"; pub const MAX_NAME: usize = 512; 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 (serde; unknown keys are errors). pub struct HonchoSection { pub url: String, pub agent: String, pub owner: String, pub trusted: String, pub untrusted: String, pub fleet: String, pub read: Vec, // #[serde(default)] } /// One `[[honcho.read]]` as written (serde; unknown keys are errors). pub struct ReadSection { pub agent: String, pub workspace: String, pub trust: Option, // #[serde(default)] pub class: Option, // #[serde(default)] } /// `[honcho]` and its keys, checked. pub struct HonchoConfig { pub url: String, pub agent: String, pub owner: String, pub trusted: String, pub untrusted: String, pub fleet: String, pub read: Vec, pub keys: BTreeMap, // by the name in [secrets.] } pub struct ReadWorkspace { pub agent: String, pub workspace: String, pub trust: Trust, pub class: DataClass, } pub enum Trust { Trusted, Untrusted, } pub fn read_key(agent: &str) -> String; // "honcho_read_" pub fn is_name(name: &str) -> bool; // Honcho's rule: 1 to 512 of a-z, A-Z, 0-9, _ and - pub fn check( section: Option<&HonchoSection>, secrets: &BTreeMap, ) -> Result, String>; ``` The derives: `HonchoSection` and `ReadSection` `Debug, Clone, PartialEq, Eq, Deserialize`; `HonchoConfig` and `ReadWorkspace` `Debug, Clone, PartialEq, Eq`; `Trust` also `Copy`. In `crates/memoryd/src/config.rs` (edits C1 to C8): `Config` gains `pub honcho: Option` and `pub secrets: BTreeMap`; `Sockets` gains `pub state: PathBuf`; `ConfigError` gains `Honcho(PathBuf, String)`; `Config` gains `pub fn honcho_config(&self) -> Result, String>` and `pub fn state_socket(&self) -> PathBuf`. ## The reasons `check` returns the first reason it finds. The order is the given `check`'s: R4 (no section), R1, R2 for the five names of `[honcho]`, R3, R5, then each `[[honcho.read]]` in turn (R2, R6, R7, R8, R9, R10), then the keys (R11, R12, R13). `{x:?}` is Rust's `Debug` format: the text in double quotes, with `"` and control characters escaped. R1 to R4 are in the given code already; R5 to R13 are yours. | # | When | The reason (the text in `Err`) | |---|---|---| | R1 | `url` is not `http://` or `https://`, a host and an optional port | `[honcho] url must be http:// or https://, a host and an optional port, and nothing else` (the constant `URL_REASON`; it never quotes the URL, which could hold a password) | | R2 | a name is not a Honcho name | `{table} {key} {name:?} is not a Honcho name: 1 to 512 of a-z, A-Z, 0-9, _ and -`, `table` being `[honcho]` or `[[honcho.read]]` (the given `check_name`) | | R3 | `agent` and `owner` are equal | `[honcho] agent and owner are both {agent:?}: the agent's peer must not be the owner's` | | R4 | a `[secrets.]` with no `[honcho]` | `[secrets.{name}] is set, but there is no [honcho] section` (the first name) | | R5 | two of `trusted`, `untrusted`, `fleet` are equal | `[honcho] workspace {name:?} is named twice: trusted, untrusted and fleet must be three different workspaces` | | R6 | a read agent is `[honcho] agent` | `[[honcho.read]] agent {agent:?} is this agent ([honcho] agent)` | | R7 | a read agent is in an earlier entry | `[[honcho.read]] agent {agent:?} is listed twice` | | R8 | a read workspace is one of the three | `[[honcho.read]] workspace {workspace:?} is one of this agent's own` | | R9 | `trust` is another text | `[[honcho.read]] trust {trust:?} of agent {agent:?} is not "trusted" or "untrusted"` | | R10 | `class` is another text | `[[honcho.read]] class {class:?} of agent {agent:?} is not "public", "private" or "secret"` | | R11 | `honcho_read_` for an agent not in `[[honcho.read]]` | `[secrets.{name}] is the key of agent {agent:?}, which is not in [[honcho.read]]` | | R12 | a `[secrets.]` with any other name | `[secrets.{name}] is not a Honcho key: memoryd has only honcho_trusted, honcho_untrusted, honcho_fleet and honcho_read_` | | R13 | `SecretSpec::source` refuses the table | `[secrets.{name}] {why}`, `why` being `source`'s own text | In Rust, R9 and R10 contain `\"` for each double quote shown: for R9, `format!("[[honcho.read]] trust {other:?} of agent {:?} is not \"trusted\" or \"untrusted\"", entry.agent)`, where `other` is the text written. In R11, `agent` is what follows `honcho_read_` in the name. `name` in R4, R11, R12 and R13 is shown as it is, without `:?`. ## Every place that changes This is the complete list. | Place | What happens there | |---|---| | `crates/memoryd/Cargo.toml` | `bxcore` with the feature `secrets` (edit K1) | | `crates/memoryd/src/lib.rs` | `pub mod honcho;` (edit L1) | | `crates/memoryd/src/honcho/mod.rs` | new: step 3 gives it whole | | `crates/memoryd/src/honcho/config.rs` | new: a skeleton you copy, then fill 7 bodies (step 4) | | `crates/memoryd/src/config.rs` | module comment, imports, `Sockets.state`, `Config.honcho` and `.secrets`, `ConfigError::Honcho`, `load`, `honcho_config`, `state_socket` (edits C1 to C8) | | `crates/memoryd/src/main.rs` | the config error's pointer by its kind (edits M1, M2) | | `docs/runbook.md` | the entry `honcho-config-invalid`, appended (step 6) | | `docs/dependencies.md` | the `zeroize` row names `memoryd` (edit D1) | | everything else in `memoryd` (`serve.rs`, `socket.rs`, …) | **no change** | ## Files - Copy, from `docs/plans/M5c1/files/` (the same path under it): - new tests: `crates/memoryd/tests/honcho_config.rs`, `crates/memoryd/tests/honcho_reasons.rs`, `crates/memoryd/tests/honcho_start.rs` - the skeleton `crates/memoryd/src/honcho/config.rs` (you fill 7 bodies in it) - Append, from `docs/plans/M5c1/files/`, to the end of `docs/runbook.md`, with one blank line before it: `runbook-honcho-config-invalid.md` (step 6 gives the command). - Create: `crates/memoryd/src/honcho/mod.rs` (step 3). - Modify: `crates/memoryd/Cargo.toml`, `crates/memoryd/src/lib.rs`, `crates/memoryd/src/config.rs`, `crates/memoryd/src/main.rs`, `docs/dependencies.md`, `docs/implementer-log.md`. - Do not touch: any other source file, any file of another crate, any other `Cargo.toml`, any other test (`crates/memoryd/tests/config.rs` in particular: it must pass as it is), the test support files, and in the skeleton anything but the 7 `todo!()` bodies. ## Facts you need - **The feature `secrets`.** `SecretSpec` and `SecretSource` are in `bxcore::secrets`, which is compiled only when a crate enables `bxcore`'s feature `secrets` (M5b). `gatewayd` does; `loopd` never does, as it holds no credentials. `memoryd` holds Honcho's keys, so edit K1 enables it. `make gate` builds the whole workspace at once, and Cargo then turns the feature on for every crate because `gatewayd` asks for it; so without K1 the gate could pass while `cargo build -p memoryd` alone fails. `Cargo.lock` does not change: `zeroize`, the feature's one dependency, is already `bxcore`'s. - **Two stages.** `Config::parse` (serde) reads the file: a missing key in `[honcho]` or `[[honcho.read]]`, an unknown key, or a value of the wrong type is a parse error, as in every other table, and keeps its pointer `memoryd-start-failed`. Then `Config::load` calls `honcho_config`, which checks the values (`honcho::config::check`): an `Err` is `ConfigError::Honcho`, and `main` prints the pointer `honcho-config-invalid` for it. `parse` itself does not check (the serve tests build their `Config` with it); `honcho_config` is pure, and later tasks call it again to get the `HonchoConfig`. - **`trust` and `class` stay text** (`Option`) in `ReadSection`, so that a wrong value is one of the reasons (R9, R10) with this runbook entry, not a serde error. Both are lower case only: `"Trusted"` and `"Private"` are refused. - **Honcho names** (M5c spec, section 6): Honcho accepts `^[a-zA-Z0-9_-]+$`, at most 512 characters, for a workspace, a peer or a session. Checked on bytes: a non-ASCII letter such as `é` fails, and so does any Unicode digit that is not `0` to `9`. - **The URL.** `http://` or `https://` (lower case), a host of ASCII letters, digits, `.` and `-`, and optionally `:` and a port from 1 to 65535 written in digits only (`u16::from_str` alone would take a leading `+`). Nothing may follow: not even `/`, since `roncho` builds each request URL from `/v3/...` and would drop a path silently. A user or password (`user:pw@host`) fails, because the host would contain `@` or the port would not be digits. The reason never quotes the URL. - **Keys are checked, not read.** `SecretSpec::source` checks the table (exactly one of `credential`, `env`, `file`; a well-formed name; an absolute path) without reading anything. The given tests name environment variables that are not set and files that do not exist, and expect `Ok`. Do not call `bxcore::secrets::load` or `read_checked`. - **Where the check runs.** `main` loads the config first, before the instance lock and before anything under the home is made (as it already does); a given test checks that `/run` and `/memory` do not exist after an invalid section. - **The given tests.** - `honcho_config.rs` (8 tests): what a valid section gives, the defaults, the keys, the key names and the pointer, parse errors, `state_socket`, and `load`. - `honcho_reasons.rs` (5 tests): every reason of the table, exactly, one problem per section; names and URLs at their edges; the first problem is the one reported. - `honcho_start.rs` (4 tests): starts `memoryd serve` as a process (every wait has a 10-second deadline; a guard kills and waits for the process before the home is removed). An invalid section: exit 1 and exactly the two lines; an unknown key: still `memoryd-start-failed`; a valid section with keys: `memoryd` serves as before. The two key values it passes (an environment variable, a 0600 file) must appear in nothing `memoryd` prints. - The calls the bodies and edits use: | Call | Signature, and what it gives | |---|---| | `str::strip_prefix` | `url.strip_prefix("https://") -> Option<&str>` | | `str::split_once` | `rest.split_once(':') -> Option<(&str, &str)>`: split at the first `:` | | `str::bytes` | `.bytes().all(\|b\| b.is_ascii_alphanumeric() \|\| b == b'-')` | | `u8::is_ascii_alphanumeric`, `u8::is_ascii_digit` | `b.is_ascii_digit() -> bool` | | `str::parse` | `port.parse::() -> Result` | | `Option::as_deref` | `entry.trust.as_deref() -> Option<&str>`, to `match` on `Some("trusted")` | | `slice::iter().any` | `earlier.iter().any(\|r\| r.agent == entry.agent) -> bool` | | `SecretSpec::source` | `pub fn source(&self) -> Result` (`bxcore::secrets`) | | `BTreeMap::insert` | `keys.insert(name.clone(), source)` | | `check_name` (given) | `fn check_name(table: &str, key: &str, name: &str) -> Result<(), String>`: `Err` is R2 | | `honcho::check` (given) | see Interfaces; `honcho_config` calls it (C7) | ## The edits, exactly Nothing else in these files changes. Each quoted "before" block occurs exactly once in its file. ### `crates/memoryd/Cargo.toml` **K1.** The line ```toml bxcore.workspace = true ``` becomes ```toml bxcore = { workspace = true, features = ["secrets"] } ``` ### `crates/memoryd/src/lib.rs` **L1.** The line ```rust pub mod files; ``` becomes ```rust pub mod files; pub mod honcho; ``` ### `crates/memoryd/src/config.rs` **C1.** The line ```rust //! `memoryd` configuration: read `memoryd.toml` into a typed `Config` (M5a spec, section 11). ``` becomes ```rust //! `memoryd` configuration: read `memoryd.toml` into a typed `Config` (M5a spec, section 11), and //! check its `[honcho]` section at start (M5c spec, section 10). ``` **C2.** The lines ```rust use std::path::{Path, PathBuf}; use bxcore::DataClass; use serde::Deserialize; ``` become ```rust use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use bxcore::DataClass; use bxcore::secrets::SecretSpec; use serde::Deserialize; use crate::honcho::config::{self as honcho, HonchoConfig, HonchoSection}; ``` **C3.** In `Sockets`, the lines ```rust pub broker: PathBuf, pub admin: PathBuf, } ``` become ```rust pub broker: PathBuf, pub admin: PathBuf, /// `brokerd`'s `state.sock`, which `memoryd` connects to, to ask for a session's state (M5c /// spec, section 7). `brokerd.toml` names the same path as its `[sockets] state`. pub state: PathBuf, } ``` **C4.** In `Config`, the lines ```rust pub sockets: Sockets, } ``` become ```rust pub sockets: Sockets, /// `[honcho]` as written; `honcho_config` checks it. Without it, Honcho is off. pub honcho: Option, /// `[secrets.]`: the Honcho keys, checked by `honcho_config`, never read here. pub secrets: BTreeMap, } ``` **C5.** The lines ```rust Parse(PathBuf, toml::de::Error), } ``` become ```rust Parse(PathBuf, toml::de::Error), /// `[honcho]` or a key for it is not allowed: the path, and `honcho_config`'s reason. Honcho(PathBuf, String), } ``` **C6.** In `impl std::fmt::Display for ConfigError`, the line ```rust ConfigError::Parse(path, err) => write!(f, "{}: {err}", path.display()), ``` becomes ```rust ConfigError::Parse(path, err) => write!(f, "{}: {err}", path.display()), ConfigError::Honcho(path, why) => write!(f, "{}: {why}", path.display()), ``` **C7.** The lines ```rust /// Read and parse `path`; a relative `soul_starter` is taken to be next to it. pub fn load(path: &Path) -> Result { let text = std::fs::read_to_string(path).map_err(|e| ConfigError::Read(path.to_path_buf(), e))?; let mut config = Config::parse(&text).map_err(|e| ConfigError::Parse(path.to_path_buf(), e))?; ``` become ```rust /// Read, parse and check `path`; a relative `soul_starter` is taken to be next to it. pub fn load(path: &Path) -> Result { let text = std::fs::read_to_string(path).map_err(|e| ConfigError::Read(path.to_path_buf(), e))?; let mut config = Config::parse(&text).map_err(|e| ConfigError::Parse(path.to_path_buf(), e))?; if let Err(why) = config.honcho_config() { return Err(ConfigError::Honcho(path.to_path_buf(), why)); } ``` and the line ```rust fn socket(&self, set: &Path, default: &str) -> PathBuf { ``` becomes ```rust /// `[honcho]` and its keys, checked: `Ok(None)` when there is no `[honcho]` and no key. pub fn honcho_config(&self) -> Result, String> { honcho::check(self.honcho.as_ref(), &self.secrets) } fn socket(&self, set: &Path, default: &str) -> PathBuf { ``` **C8.** The lines ```rust self.socket(&self.sockets.admin, "run/owner-memory/admin.sock") } ``` become ```rust self.socket(&self.sockets.admin, "run/owner-memory/admin.sock") } pub fn state_socket(&self) -> PathBuf { self.socket(&self.sockets.state, "run/memory-broker/state.sock") } ``` ### `crates/memoryd/src/main.rs` **M1.** The line ```rust use memoryd::config::Config; ``` becomes ```rust use memoryd::config::{Config, ConfigError}; use memoryd::honcho::config::RUNBOOK as HONCHO_INVALID; ``` **M2.** The lines ```rust let config = match Config::load(&path) { Ok(config) => config, Err(e) => { eprintln!("memoryd: {e}\n{START_FAILED}"); return ExitCode::from(1); } ``` become ```rust let config = match Config::load(&path) { Ok(config) => config, Err(e) => { let pointer = match &e { ConfigError::Read(..) | ConfigError::Parse(..) => START_FAILED, ConfigError::Honcho(..) => HONCHO_INVALID, }; eprintln!("memoryd: {e}\n{pointer}"); return ExitCode::from(1); } ``` ### `docs/dependencies.md` **D1.** The line (the `zeroize` row) ```text | `zeroize` | 1.9.0 | `bxcore`, feature `secrets` (enabled by `gatewayd`; never by `loopd`) | Wipes a secret's memory when it is dropped (`Secret`). Already a dependency of `rustls`. Apache-2.0 OR MIT. | ``` becomes ```text | `zeroize` | 1.9.0 | `bxcore`, feature `secrets` (enabled by `gatewayd` and `memoryd`; never by `loopd`) | Wipes a secret's memory when it is dropped (`Secret`). Already a dependency of `rustls`. Apache-2.0 OR MIT. | ``` ## Steps - [ ] **1. Copy the given tests.** From the repository root, three commands: ```sh cp docs/plans/M5c1/files/crates/memoryd/tests/honcho_config.rs crates/memoryd/tests/ cp docs/plans/M5c1/files/crates/memoryd/tests/honcho_reasons.rs crates/memoryd/tests/ cp docs/plans/M5c1/files/crates/memoryd/tests/honcho_start.rs crates/memoryd/tests/ ``` - [ ] **2. See them fail.** Two commands, each with a shell timeout of 600000 ms: - `cargo build --tests --keep-going -p memoryd`: two test targets do not compile. `honcho_config`, with 8 errors: one `E0433` (``cannot find `honcho` in `memoryd` ``), one `E0432` (``unresolved import `bxcore::secrets` ``: the feature is not on yet), two `E0599` for `honcho_config`, three `E0599` for `state_socket` (``no method named … found for struct `Config` ``) and one `E0599` for the variant `Honcho` of `ConfigError`. `honcho_reasons`, with 2: the same `E0433`, and one `E0599` for `honcho_config`. `honcho_start` compiles. - `cargo test -p memoryd --test honcho_start`: it compiles, and its 4 tests fail (about 10 s: one waits out its deadline). Each `memoryd` stopped with ``unknown field `honcho`, expected one of `paths`, `memory`, `index`, `sockets` `` and the pointer `memoryd-start-failed`: | Test | Before this task | |---|---| | `an_invalid_section_stops_memoryd_with_its_reason_and_the_pointer` | `assertion left == right failed`: the parse error where the reason was wanted | | `a_key_for_an_agent_not_read_stops_memoryd` | the same | | `an_unknown_key_in_the_section_is_still_a_start_failure` | the stderr, printed as the message: it names `honcho`, not `fleat` | | `a_valid_section_with_keys_starts_memoryd_as_before` | `memoryd never answered; stderr: …` after 10 s | - [ ] **3. The module.** Make edits K1 and L1. Create `crates/memoryd/src/honcho/mod.rs` with exactly this content: ```rust //! Honcho, the memory this agent shares with the fleet, reached through `roncho` (M5c spec). pub mod config; ``` Then copy the skeleton: ```sh cp docs/plans/M5c1/files/crates/memoryd/src/honcho/config.rs crates/memoryd/src/honcho/ ``` `cargo check -p memoryd`: no error, and 13 warnings, all from the unfilled bodies: ten `unused variable` (`name`, `url`, `section` twice, `entry` three times, `earlier`, `secrets`, `read`), ``constant `URL_REASON` is never used``, and ``function `trust_of` is never used`` and the same for `class_of`. They go as you fill the bodies. - [ ] **4. Fill the seven bodies of `honcho/config.rs`, one at a time, in this order**, each from its comment and the table "The reasons", and run `cargo check -p memoryd` after each: `is_name`, `check_url`, `check_own_workspaces`, `trust_of`, `class_of`, `read_workspace`, `check_keys`. When a body is filled, delete its instruction comment (the `//` lines inside the body) and keep the function's doc comment (`///`) as it is. Change nothing else in the file. After the seventh, `cargo check -p memoryd` prints no warning. - [ ] **5. The configuration.** Make edits C1 to C8 in `config.rs`, then M1 and M2 in `main.rs`. `cargo check -p memoryd`: no error and no warning. Then this command, and every test passes (8, 5, 4 and 4): ```sh cargo test -p memoryd --test honcho_config --test honcho_reasons --test honcho_start \ --test config ``` - [ ] **6. The runbook and the dependency row.** From the repository root: ```sh { echo; cat docs/plans/M5c1/files/runbook-honcho-config-invalid.md; } >> docs/runbook.md ``` Then `grep -c '^## honcho-config-invalid$' docs/runbook.md` prints `1`. Make edit D1. - [ ] **7. Check.** Four commands: - `cargo build -p memoryd`: it builds (it needs K1: see "The feature `secrets`"). - `grep -rn 'runbook.md#honcho-config-invalid' crates/memoryd/src` prints exactly one line, the `RUNBOOK` constant in `honcho/config.rs`. - `grep -n 'secrets::load\|read_checked' crates/memoryd/src -r` prints nothing: no key is read. - `git diff --stat -- crates/bxcore crates/loopd crates/brokerd crates/gatewayd crates/bxctl` prints nothing. - [ ] **8. Run the gate.** `cargo fmt --all`, then `make gate`, with a shell timeout of 600000 ms. Expected last line: `gate: ok`, with 1188 tests: 1171 before this task, plus 17 new ones (8 in `honcho_config.rs`, 5 in `honcho_reasons.rs`, 4 in `honcho_start.rs`). - [ ] **9. Log and commit.** Add your row to `docs/implementer-log.md`; its Task cell is `M5c1/06-memoryd-honcho-config`. `git status --short` lists `crates/memoryd/Cargo.toml`, `crates/memoryd/src/lib.rs`, `crates/memoryd/src/config.rs`, `crates/memoryd/src/main.rs`, `docs/runbook.md`, `docs/dependencies.md` and your log as modified, and `crates/memoryd/src/honcho/` and the three test files as new. Stage with `git add crates/memoryd docs/runbook.md docs/dependencies.md docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says. `Cargo.lock` does not change in this task; the `git add` is harmless. ## Done when - `make gate` prints `gate: ok` with 1188 tests. - `git diff HEAD~1 --stat` lists only the four modified `memoryd` files, `honcho/mod.rs`, `honcho/config.rs`, the three new test files, `docs/runbook.md`, `docs/dependencies.md` and `docs/implementer-log.md`. ## Stop and report if - A "before" block of K1, L1, C1 to C8, M1, M2 or D1 is not in its file exactly as shown, or is there more than once. - Step 2 or 3 shows a different result: other test targets, errors of another kind, other failing tests, or other warnings. - `cargo check` reports an error after a body you filled or an edit you made, and the comment or the edit does not tell you how to fix it. Quote the error, and stop. - A comment in the skeleton does not fit the code under it, or a reason in the table does not fit the comment that names it. Quote both and stop. - After step 4 or 5 a test fails although each body follows its comment and each edit is as shown. Do not change a test or the skeleton's given code: stop, and give the failing tests' names and their messages. - The fix seems to need a change anywhere else: `serve.rs`, another crate, the test support files, reading a key, or a check the table does not list. It does not; stop and say why you think so. - A test outside these three files fails in the gate: check once whether it fails without your change, then stop and report both results. - The gate passes with a count other than 1188.