diff --git a/docs/decisions.md b/docs/decisions.md index 257232c..94a96e5 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -6,6 +6,7 @@ Newest first. A decision that changes `docs/design.md` lands in the same commit | Date | Decision | Reason | |---|---|---| +| 2026-09-25 | `memoryd` reaches Honcho through the owner's `roncho`, not a client of its own. `roncho` first gains a blocking client with a small dependency graph (no async runtime), no default server, no hidden environment settings or redirects, and request shapes checked against rift; the requirements are `docs/specs/2026-09-25-roncho-requirements.md`, handed to `roncho`'s developer. Boxmaker will allow that one git source in `deny.toml`, pinned by tag. | Owner's decision (M5b design). One client for the fleet. As it stands `roncho` is async only (tokio, reqwest), defaults to `api.honcho.dev`, and differs from rift in its search shapes, so it cannot be used unchanged. | | 2026-09-24 | M5a's check on memory writes stays short and fixed: control characters, invisible characters, chat-template markup, six phrases. A research thread on prompt-injection defences feeds a sanitizer module for memory writes (and possibly other persisted text), built as hardening in M5b. Memory writes are not posted to the chat; `bxctl memory log` lists them. | Owner. The archive routing of untrusted sessions' writes is the main defence; a pattern list is defence in depth and should rest on research, not guesses. | | 2026-09-24 | Memory writes need no approval, as in Hermes (the owner's `~/.hermes` has no `write_approval`). What a session that has seen untrusted content wants remembered becomes a note, labelled untrusted, not identity in the prompt (refined the same day: identity and experience are separate kinds of memory). Every write is scanned, journalled (listed by `bxctl memory log`, not posted anywhere) and can be undone; nothing is deleted. Brief change P21 proposed; the M5a spec is revised to match. Session-scoped approvals for other tools become an open decision. | Owner: an agent harness should act without a stream of questions, and reading and writing memory is what an agent is. The only risk the approval guarded is injected text reaching every future prompt, and that needs a session that read untrusted content; `brokerd` knows which sessions those are. | | 2026-09-24 | M5a spec review: the policy of `recall` (`auto`) and `memory` (`ask`) is built into `brokerd`, and grant files cannot name them; tool calls are indexed by name and arguments, tool results are not; summaries of tool output, where a tool opts in, come with M5c's summariser. | Owner: an agent that cannot write memories is no better than a plain chat window, and a grant file must not be able to turn edits off or make them unattended. A web fetch's summary is worth keeping; most tools' output is not. | diff --git a/docs/specs/2026-09-25-roncho-requirements.md b/docs/specs/2026-09-25-roncho-requirements.md new file mode 100644 index 0000000..a4cd2ff --- /dev/null +++ b/docs/specs/2026-09-25-roncho-requirements.md @@ -0,0 +1,197 @@ +# roncho: what Boxmaker's `memoryd` needs from it + +**For:** the agent that develops `roncho` (`git.wntrmute.dev/kyle/roncho`, `~/src/roncho` on titan). +**From:** Boxmaker's design model, 2026-09-25, for milestone M5b. **Status:** requirements, for the +owner to hand over. Boxmaker's M5b spec is written against the release this produces. Code +references are to `roncho` at `f24fc4a` ("Initial commit"). + +## 1. Why + +Boxmaker is the owner's personal agent harness (Rust, `~/src/boxmaker`). Its memory daemon, +`memoryd`, will sync finished conversation turns to the owner's Honcho on rift, write some +entries as conclusions, search Honcho, and ask its dialectic. The owner wants it to do that +through `roncho`, not through a second client. Today `roncho` cannot be used from Boxmaker: + +- It is async only (tokio, reqwest, hyper, futures). Boxmaker has a standing rule: blocking I/O + with threads, no async runtime, and `memoryd` is the smallest-authority daemon of the set. +- Some of its request and response shapes disagree with the live server. sift's Go client, + checked against rift on 2026-09-22, found that workspace search takes `{"query", "limit"}` and + returns a bare JSON array, not a `Page` (`~/src/sift/internal/fleetctx/fleetctx.go:115-147`, + `~/src/sift/docs/P2.md:103-105`). +- It defaults to `https://api.honcho.dev`. For Boxmaker, memory text must never leave the owner's + hosts, so a default that points at a cloud service is a hazard, not a convenience. + +Boxmaker's policy (which turns are synced, trust labels, taint) stays in `memoryd`. `roncho` is +the transport and the types: correct, small, and predictable. + +## 2. The deployment it must work against + +| Fact | Value | Source | +|---|---|---| +| Honcho API | `http://rift.scylla-hammerhead.ts.net:8000`, plain HTTP over the tailnet | `imladris/hw/orion/default.nix:105`; `sift/docs/PLAN.md:38` | +| Version | 3.2.0, image `3.2.0-pr1092-7e57fe2` (roncho's SPEC targets 3.2.1) | `imladris/docs/metrics-plan.md` | +| Authentication | apparently none: sift sends no `Authorization` header and succeeds. **Not confirmed.** | `sift/internal/fleetctx/fleetctx.go:117-127` | +| Models | deriver, summaries and dialectic on dixie's `ornith-1.5-9b-uncensored`; dialectic answers take 30 to 50 s | `imladris/hw/dixie/default.nix`; `sift/docs/PLAN.md:236-237` | +| Workspaces in use | `hermes-fleet`, `guardian-star` (other agents'); each Boxmaker will have its own | `imladris/hw/orion/default.nix:106-107` | + +**Never write to a workspace you did not create for testing.** `hermes-fleet` and +`guardian-star` hold other agents' live memory. Write tests go to a throwaway workspace (for +example `roncho-test-`), deleted afterwards. + +## 3. Requirements + +MUST: Boxmaker cannot use `roncho` without it. SHOULD: Boxmaker can work around it, at a cost. + +### 3.1 A blocking client with a small dependency graph + +- **R1 (MUST)** A blocking API: every operation in 3.3 as a plain function call that returns when + the response is read. No async runtime is started, required or depended on. +- **R2 (MUST)** Selected by Cargo features so a consumer can build **only** the blocking client: + `roncho = { …, default-features = false, features = ["blocking"] }` (the name is yours). With + that, the dependency graph contains none of `tokio`, `reqwest`, `hyper`, `h2`, `futures`, + `async-stream`, `bytes`, `webpki-roots`. The async client may stay as the default feature. +- **R3 (MUST)** HTTP/1.1 over plain TCP for `http://` URLs, written on `std::net` or with a + small blocking crate. Boxmaker already has two such clients (`crates/loopd/src/http.rs`, + `crates/gatewayd/src/http.rs`): one request per connection, `Connection: close`, chunked and + `Content-Length` bodies. +- **R4 (SHOULD)** `https://` behind an optional feature, using `rustls` 0.23 with the `ring` + provider and the platform's certificates (`rustls-native-certs` 0.8). Not `webpki-roots`, + not `aws-lc-rs`. rift needs no TLS today, so this may come after the rest. +- **R5 (MUST)** Every crate in the blocking graph (with and without R4) is on crates.io, is + licensed under one of MIT, Apache-2.0, ISC, BSD-3-Clause, Unicode-3.0 or Zlib, and does not + duplicate a different major version of a crate Boxmaker already locks. Boxmaker locks today: + `serde` 1.0.229, `serde_json` 1.0.151, `rustls` 0.23.45, `ring` 0.17.14, + `rustls-native-certs` 0.8.4, `thiserror` 2.0.21, `syn` 3, `toml` 1.1.6, `zeroize` 1.9.0. + Boxmaker's `cargo deny` runs with `multiple-versions = "deny"`. +- **R6 (SHOULD)** Keep the blocking graph small: beyond `serde` and `serde_json` (and R4's TLS + crates), add as few crates as you can. Prefer timestamps as RFC 3339 strings, ids as strings and + URLs parsed by hand over `chrono`, `uuid` and `url` in the blocking models. If you keep any of + them, use `default-features = false` (for `chrono`, no `clock`, which pulls platform time-zone + crates) and say why in the README. `thiserror` is acceptable: Boxmaker already locks it. +- **R7 (MUST)** Builds and passes its tests with Rust 1.95 (Boxmaker's `rust-version`). Set + `rust-version` in `Cargo.toml`. +- **R8 (MUST)** A git tag per release that Boxmaker can pin (`rev` or `tag`), and a changelog + entry saying what changed in the blocking API. Boxmaker will add the git source to its + `deny.toml` allow-list for this one URL; publishing to crates.io instead is fine too. + +### 3.2 Configuration and safety + +- **R9 (MUST)** No default base URL in the blocking client. Building a client without one is an + error. (Keep the async default if you like; the blocking path must not have one.) +- **R10 (MUST)** The API key is optional. When set, it is sent as `Authorization: Bearer`; when + not, no `Authorization` header at all. Building without a key is not an error. The key never + appears in an error, a `Debug` output or a log line. +- **R11 (MUST)** The workspace is set explicitly per client. A client is cheap to clone and one + client serves one workspace, as today. A client is `Send + Sync`. +- **R12 (MUST)** The blocking client takes its configuration only from the caller. It reads no + environment variables: not `HONCHO_*` (today `build()` falls back to `HONCHO_WORKSPACE_ID` and + `HONCHO_API_KEY`, `src/client.rs:244-253`), not proxy settings (`HTTP_PROXY`, `ALL_PROXY` and so + on). It does not follow redirects: a 3xx is returned as an error. Memory text is sent only to + the configured host. +- **R13 (MUST)** Timeouts are set by the caller, per client: a connect timeout, and a read + timeout that means "no bytes for N seconds", not a total deadline. A dialectic answer takes 30 + to 50 s on the current hardware, so the caller will set a long one there. No hidden retries: + either implement `max_retries` for idempotent calls with a documented policy, or remove it. The + blocking client defaults to no retries. +- **R14 (MUST)** A cap on the size of a response body, set by the caller, with a sensible + default (for example 8 MiB). A body over the cap is an error, and reading stops at the cap. +- **R15 (MUST)** No background threads, no global state, no logging of request or response + content, no telemetry, no update checks. + +### 3.3 Operations the blocking API provides + +Each operation is verified against rift (3.4) before it is released. Names are suggestions. + +| # | Operation | Honcho endpoint (as roncho has it today; verify) | Boxmaker's use | +|---|---|---|---| +| O1 | get or create the workspace | not in roncho at `f24fc4a`; find it in Honcho 3.2's docs | first start | +| O2 | get or create a peer, with metadata and configuration | `POST peers` | the owner, the agent, other agents met in channels | +| O3 | get or create a session, with its peers and each peer's `observe_me` / `observe_others` | `POST sessions` | one Honcho session per Boxmaker session | +| O4 | add messages in a batch, each with `peer_id`, `content`, `metadata` (a JSON object) and `created_at` | `POST sessions/{s}/messages` | the sync of finished turns; the metadata holds Boxmaker's session id, record number and labels | +| O5 | list a session's messages, paged, with filters | `POST sessions/{s}/messages/list` | checking what a sync already sent | +| O6 | delete a session | `DELETE sessions/{s}` | rebuilding | +| O7 | delete the workspace, and report the "active sessions" conflict (409) as its own error, if Honcho has one | not in roncho at `f24fc4a` | rebuilding | +| O8 | create conclusions in a batch (1 to 100), `content`, `observer_id`, `observed_id`, optional `session_id` | `POST conclusions` | entries moved out of identity; trusted notes | +| O9 | list, query and delete conclusions | `POST conclusions/list`, `POST conclusions/query`, `DELETE conclusions/{id}` | rebuilding; checks | +| O10 | search the workspace, with a result limit | `POST search` (rift: `{"query", "limit"}` and a bare array) | `recall` | +| O11 | search a peer's messages and a session's messages, with a limit | `POST peers/{p}/search`, `POST sessions/{s}/search` | `recall`, narrowed | +| O12 | the dialectic, non-streaming: query, target, session, reasoning level, whether to include evidence | `POST peers/{p}/chat`, `POST chat` | `ask_memory` | +| O13 | a cheap probe that the server is up and speaks the expected version | whatever Honcho 3.2 offers; if nothing, a cheap read (O1's get) | `memoryd`'s startup and health | + +- **R16 (MUST)** O1 to O13 in the blocking API, with typed requests and responses. Fields the + server may add later are ignored when decoding; fields Boxmaker relies on are required. +- **R17 (SHOULD)** O12 streaming, with the read timeout of R13 applying between chunks, so a + caller can tell a slow answer from a dead one. The SSE must be parsed by lines across network + chunks (see R20). +- **R18 (MUST)** One error type for the blocking client that lets the caller tell apart: cannot + connect, timed out (no bytes), HTTP status with the first 4 KiB of the body, response too large, + response not decodable, invalid configuration, and the workspace-delete conflict. Implements + `std::error::Error` and `Display`; never contains the API key. + +### 3.4 Correctness against rift + +- **R19 (MUST)** Every O-operation is exercised against rift 3.2.0 in a throwaway workspace (reads + may use `hermes-fleet`, writes never), and the result recorded in a compatibility document in + the repository: endpoint, method, request shape, response shape, the date, and the server's + version. Where Honcho's docs and rift disagree, rift wins and the document says so. +- **R20 (MUST)** Fix these, found reading the current code, in the async client too: + - search request and response shapes (O10, O11): rift takes `{"query", "limit"}` at the top + level and returns a bare array (sift, above). roncho's workspace search sends `{"query"}` with + no limit and expects a `Page` (`src/api/workspace.rs:13-19`); peer and session search send + `{"filters": {"query"}}` and expect a `Page` (`src/api/peers.rs:174-184`, + `src/api/sessions.rs:178-186`). + - `list_messages` computes the `page`, `size` and `reverse` query but never sends it + (`src/api/sessions.rs:110-120`), so a paged read can loop on the first page. + - `Message.role` is required when decoding (`src/models/message.rs:5-16`); sift's verified field + list has no `role`. + - peer and session lists use `GET` (`src/api/peers.rs:23-28`, `src/api/sessions.rs:18-23`); + Honcho 3's other lists are `POST …/list`. Settle on what rift does, and the same for + `peers/{p}/context` (`src/resources/peer.rs:136-141`). + - the SSE reader splits each network chunk into lines on its own, so a line split across two + chunks is misparsed (`src/api/peers.rs:122-135`, `src/api/workspace.rs:107-120`). + - `build()` reports a missing workspace as `MissingApiKey` (`src/client.rs:244-248`). + - `max_retries` is stored and never read (`src/client.rs:20`, `264`; R13). +- **R21 (MUST)** Tests for the blocking client against a fake server in the test process + (`std::net::TcpListener`), covering each O-operation's request bytes and response decoding, and + also: a refused connection, a server that sends nothing (read timeout), a server that stops + mid-body, a redirect (refused), an oversized body (capped), a non-JSON body, a 409 on workspace + delete, and that no `Authorization` header is sent without a key. Every wait in a test has a + limit. +- **R22 (SHOULD)** A live suite, ignored by default, run with an environment variable naming the + server, that performs R19's checks and deletes its workspace at the end. + +### 3.5 Semantics to find out and write down + +Boxmaker's design depends on these answers. For each, check Honcho 3.2's documentation and rift, +and record what you found (or "not found") in the compatibility document. Do not guess. + +- **Q1** Is authentication off on rift? If it can be on, are keys scoped per workspace, and is + there a read-only key? +- **Q2** Can a single message be deleted or edited in 3.2.0? What does deleting a session remove + from conclusions, representations and summaries derived from it? +- **Q3** The deriver: what triggers it (per message, per batch, per session), how long until + conclusions appear under normal load, how to see whether a message has been processed, and which + peer, session or workspace configuration keys turn derivation off for a peer. +- **Q4** Message metadata: can search (O10, O11) and the dialectic (O12) be filtered by metadata + (so that labelled messages can be excluded), and what is the filter grammar? +- **Q5** What does a conclusion's `source_ids` refer to? Can a conclusion be traced to the + messages it came from? +- **Q6** When does workspace deletion return 409 ("active sessions"), and how does a caller make a + session inactive? +- **Q7** Is there a health or version endpoint (O13)? +- **Q8** Does the `3.2.0-pr1092` image differ from upstream 3.2.0 or 3.2.1 in any endpoint above? + +## 4. Out of scope for roncho + +- Which turns are synced, trust per workspace, taint, sanitising text: Boxmaker's `memoryd`. +- Queues and progress tracking of a sync: `memoryd` keeps its own. +- A CLI. `roncho` has none at `f24fc4a`, and Boxmaker does not need one. + +## 5. Done when + +- A tagged release exists whose blocking feature meets R1 to R18, with the R20 fixes. +- `cargo tree -e normal --no-default-features -F blocking` (with your feature name) shows no + crate from R2's list, and every crate meets R5. +- The compatibility document covers O1 to O13 and Q1 to Q8, dated, with rift's version. +- The fake-server tests of R21 pass with Rust 1.95; the live suite of R22 passes against rift. +- The owner is told the tag, and anything in this document that could not be done, and why.