What Boxmaker checked of v0.1.0 and the fixes on top of it: the blocking library now builds and its tests pass, but the quoted blocking-only test command does not compile (ungated async test files), and nothing is committed or tagged yet. Five MUST items and nine SHOULD items remain, a queue-status operation among them. New requirements R23 to R27 prepare for rift turning on API keys: 401/403 as their own error, an empty key refused, one key per client, keys wiped on drop, and the live suite able to run with a key. Choices roncho made that Boxmaker accepts are listed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
297 lines
21 KiB
Markdown
297 lines
21 KiB
Markdown
# 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.
|
|
`roncho` was being reworked while this was written: the divergences in 3.4 were last checked at
|
|
`a9708f1` and are described by behaviour, not by line. Check each one before fixing it.
|
|
|
|
## 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`,
|
|
`~/src/sift/docs/P2.md`).
|
|
- 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`; `sift/docs/PLAN.md` |
|
|
| 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` |
|
|
| 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` |
|
|
| Workspaces in use | `hermes-fleet`, `guardian-star` (other agents'); each Boxmaker will have its own | `imladris/hw/orion/default.nix` |
|
|
|
|
**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-<date>`), 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`,
|
|
`HONCHO_API_KEY` and `HONCHO_BASE_URL`), 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 | `POST /v3/workspaces` (in the async client as `get_or_create_workspace`) | 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 | `DELETE /v3/workspaces/{id}` (the async `delete_workspace` returns it as a generic error) | 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 divergences, found by reading the async client; they apply to the
|
|
blocking client too, and any fix to shared models or request code applies to both:
|
|
- **Search shapes (O10, O11).** rift takes `{"query", "limit"}` at the top level and answers
|
|
with a bare array (sift, above). roncho's workspace search sends `{"query"}` with no limit, and
|
|
its peer and session searches send `{"filters": {"query"}}`; all three decode the answer as a
|
|
`Page`. There is no way to set a limit.
|
|
- **Paging of messages (O5).** `list_messages` builds the `page`, `size` and `reverse` query and
|
|
never sends it, so a paged read can keep getting the first page.
|
|
- **Message fields.** `Message.role` is required when decoding; sift's verified field list has no
|
|
`role`. Make every field Boxmaker does not need optional, and check which fields rift sends.
|
|
- **List methods.** Peer and session lists use `GET`, while Honcho 3's other lists (messages,
|
|
conclusions, workspaces) are `POST …/list`. Settle on what rift does, and the same for
|
|
`peers/{p}/context`.
|
|
- **Streaming.** The SSE reader splits each network chunk into lines on its own, so a line split
|
|
across two chunks is misparsed. Buffer until a newline.
|
|
- **Build errors.** A missing workspace is reported as `MissingApiKey`, and a missing key is an
|
|
error at all (R10).
|
|
- **Retries.** `max_retries` is stored and never read (R13).
|
|
- **Workspace delete.** The "active sessions" conflict is not told apart from other failures
|
|
(O7, R18).
|
|
- **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. 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.
|
|
|
|
## 6. Addendum: status at `v0.1.0`, and API keys (2026-09-25, later)
|
|
|
|
Boxmaker's review of `v0.1.0` (`1e58d25`) and of the fixes on top of it, which were still
|
|
uncommitted when checked. Everything marked "checked" was run by Boxmaker's design model on titan
|
|
with Rust 1.95, `--offline`, in a scratch target directory; "reported" is the `roncho` agent's word,
|
|
not yet checked here.
|
|
|
|
### 6.1 Fixed since `v0.1.0`
|
|
|
|
| Item | State |
|
|
|---|---|
|
|
| The blocking-only library did not build (`src/error.rs` imported `reqwest`, `src/models/chat.rs` imported `futures`) | checked: `cargo build --no-default-features -F blocking` builds |
|
|
| `ClientBuilder` derived `Debug` and printed the key | checked: the only `Debug` derive left in `src/blocking/client.rs` is on `Probe` |
|
|
| Streaming never sent `"stream": true`, and decoded UTF-8 per socket read | checked: `"stream"` is sent; the UTF-8 test reported |
|
|
| Request bodies not asserted; the live suite passed when unset; 200/201 and the 202 body contradicted each other in the compatibility table | reported |
|
|
| Tests of the blocking client | checked: `--lib --test blocking_test` with `-F blocking`: 3 and 9 pass, 1 ignored; with `-F tls`: 4 and 9 pass; the default suite passes |
|
|
|
|
### 6.2 Still to do (MUST)
|
|
|
|
- **A1** `cargo test --no-default-features -F blocking`, the command quoted as passing, does not
|
|
compile: the async test files (`smoke_test.rs`, `client_test.rs` and the others that
|
|
`smoke_test.rs` includes) use `roncho::Honcho` and `futures` without a feature gate. Gate each
|
|
with `#![cfg(feature = "async")]`, and run both `-F blocking` and `-F tls` (default features
|
|
off) wherever the tests are run, so the next break is caught.
|
|
- **A2** Commit, push, and tag a new release from a commit that is on `origin`. The tag's message
|
|
must name the compatibility document by the name it has at that commit (`v0.1.0` names
|
|
`docs/rift-2026-09-25.md`, which the working tree deletes).
|
|
- **A3** `query_conclusions`: rift refuses a query without the observer and observed filters. Make
|
|
them required in the blocking request type, or refuse before sending.
|
|
- **A4** `create_conclusions`: refuse a batch of 0 or more than 100 before sending.
|
|
- **A5** Blocking chat drops `response_format` without a word. Send it, or remove it from what the
|
|
blocking options accept.
|
|
|
|
### 6.3 Still to do (SHOULD)
|
|
|
|
- **A6** Version requirements: `rustls = "=0.23.45"` and `rustls-native-certs = "=0.8.4"` are exact
|
|
pins, so every patch release Boxmaker takes would need a `roncho` release. Use the ordinary
|
|
caret form (`"0.23.45"`, `"0.8.4"`). Cargo still unifies compatible versions, so Boxmaker's
|
|
no-duplicates rule holds.
|
|
- **A7** `dial` tries only the first address a name resolves to. Try each in turn, with the
|
|
connect timeout per address: a tailnet name can resolve to IPv6 and IPv4, and one may be
|
|
unreachable. Say in the README that name resolution itself is not bounded by the timeout.
|
|
- **A8** `Message` requires `session_id`, `workspace_id`, `created_at` and `token_count`. Keep
|
|
required only what Boxmaker reads (`id`, `content`, `peer_id`, `session_id`, `created_at`,
|
|
`metadata`); make the rest optional, so a server that omits one does not fail a whole search.
|
|
- **A9** Mark the blocking `Error` `#[non_exhaustive]`, so the variants of 6.4 are not a breaking
|
|
change.
|
|
- **A10** O14, a new operation: the workspace's queue status
|
|
(`GET /v3/workspaces/{id}/queue/status`, from Q3), so Boxmaker can tell when derivation has
|
|
caught up after a sync or a rebuild.
|
|
- **A11** Tests: the body cap on chunked and close-delimited bodies (only `Content-Length` is
|
|
covered); the request of workspace chat and of the probe; a limit on the fake servers' `accept`.
|
|
- **A12** Live suite: workspace chat, streaming and `include_evidence`; the time until a conclusion
|
|
appears for a new message (Q3), measured a few times; and whether `observe_me: false` and
|
|
`reasoning.enabled: false` stop derivation (Q3), since Boxmaker may use them.
|
|
- **A13** The async client's share of R20: the 409 on workspace delete as its own error, the key
|
|
optional, and the retry policy written down. Boxmaker uses only the blocking client, so this is
|
|
for `roncho`'s other users.
|
|
- **A14** `SPEC.md` and `AGENTS.md` still say "async throughout" and list `chrono` and `uuid`; the
|
|
README's roadmap still lists a blocking API as out of scope.
|
|
|
|
### 6.4 API keys (new: MUST unless marked)
|
|
|
|
Rift will turn authentication on. Q1 found that keys are scoped (to a workspace, a peer or a
|
|
session) and that key creation is disabled on rift today (`405 "Feature is disabled"`).
|
|
|
|
- **R23** A 401 or 403 is its own error variant (for example `Unauthorized { status }`), not a
|
|
generic status, so `memoryd` can say "the key for workspace X was refused" and point to a runbook
|
|
entry.
|
|
- **R24** A key set explicitly but empty is a configuration error, not "no key". With keys on,
|
|
silently sending no `Authorization` header hides a mistake. An absent key still means no header
|
|
(R10).
|
|
- **R25** Each client carries its own key, since keys are per workspace: Boxmaker will build one
|
|
client per workspace (its trusted and untrusted workspaces, and each fleet workspace it reads),
|
|
each with that workspace's key. Say so in the README.
|
|
- **R26 (SHOULD)** Hold the key in memory that is wiped when the client is dropped (`zeroize`, which
|
|
Boxmaker already locks at 1.9.0), and never copy it into an error or `Debug` output (R10).
|
|
- **R27 (SHOULD)** The live suite takes a key from the environment (for example
|
|
`RONCHO_LIVE_KEY`), so it can run once rift has authentication, and checks a refused key
|
|
(R23). When rift's authentication is on, re-answer Q1 there: what a workspace key may do in
|
|
another workspace, and whether a peer-scoped key is read-only as the docs say.
|
|
|
|
### 6.5 Accepted as `roncho` has them
|
|
|
|
- The features `blocking` and `tls` (implying `blocking`), with async on by default; Boxmaker uses
|
|
`default-features = false`.
|
|
- Defaults for the timeouts (10 s connect, 120 s idle read) and the body cap (8 MiB), which the
|
|
caller overrides; the write timeout reusing the read timeout.
|
|
- The probe as `GET /health` returning a status. It cannot report a version, since Honcho 3.2 has
|
|
no version route.
|
|
- Streaming as an `Iterator`; models shared with the async client, timestamps as strings.
|
|
- The compatibility document not naming rift or its address, if `roncho` is to be public.
|
|
Boxmaker keeps those facts in its own documents. The document should still give the date and
|
|
the Honcho version and image as far as they are known.
|