Files
boxmaker/docs/specs/2026-09-25-roncho-requirements.md
T
2026-09-28 00:38:43 -07:00

21 KiB

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 and others (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 the other existing workspaces 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.