# DAEMON → PROTO — requests against `contracts/` (and its codegen) Status: **open**. Raised by lane DAEMON while building `rpc/` against `1.3.0`. PROTO owns `contracts/`, including `contracts/codegen/`. Ranking per `contracts/README.md` rule 4: a codegen output-shape change that every server must adopt is effectively **major for the C++ binding** even when the wire is untouched — it needs a version note and a regen, not a silent change. --- ## Status - **P1 — landed** on `lane/proto` as `contracts/` **1.4.0** (commit `5e3e215`), as the `HandlerError` / `HandlerResult` sketch below. Wire is byte-identical; C++-binding bump only. `rpc/` adopts it (the predicted `Result` → `HandlerResult` swap on the `on_*` overrides) **once `lane/proto` merges to `main`** — not against the unmerged branch. `uds_roundtrip`'s `-32603`-collapse guard flips to `-32010` in the same change. - **P2 — resolved.** `session.hello.version-mismatch`'s `data.expected` is now `$any`; the error-fixture compare is on `code` only, so `rpc/` echoes `kProtocolVersion` there. PROTO's writeup: `contracts/proto-answers-daemon-m1.md`. --- ## P1. The generated `Dispatcher` has no error channel below `-32603` — **blocking a conformant server** `velox::proto::Dispatcher`'s 39 methods each return `Result` = `std::expected`, and `dispatch()` maps **every** handler error to `ErrorCode::InternalError` (`-32603`): ```cpp auto r = handler.on_download_get(*p); if (!r) return make_error(id, ErrorCode::InternalError, r.error().message, nlohmann::json{{"path", r.error().path}}); ``` So a handler cannot return any of the contract's own error codes. The error fixtures in `contracts/fixtures/errors/` that a live server must satisfy (DAEMON DoD: "passes the full conformance suite as a server, over both transports") include: | Fixture | Expected code | `data` | Originates | |---|---|---|---| | `download.get.not-found` | `-32010` | `{taskId}` | inside the handler | | `download.add.invalid-path` | `-32011` | `{path}` | inside the handler (after canonicalization) | | `download.probe.probe-failed` | `-32013` | `{httpStatus}` | inside the handler | | `session.pair.rate-limited` | `-32014` | `{retryAfterSec}` | server-side gate, but cleanest expressed as a handler result | | `session.hello.version-mismatch` | `-32001` | `{expected, actual}` | can be done server-side around `dispatch()` | | `session.hello.not-paired` | `-32002` | — | server-side WS auth gate, around `dispatch()` | `-32001`, `-32002`, `-32003` DAEMON can and will handle in the server layer that wraps `dispatch()` (`-32003` is already in `dispatch()` itself). But `-32010`, `-32011`, `-32013` are per-method **handler outcomes** — the daemon knows "no such task" only after the store lookup, "outside allowed roots" only after `realpath()`. There is no correct way to surface them today except misreporting as `-32603`, which the TS conformance replay will reject on the `code` compare. **Requested:** give the generated handler methods an error return that carries an `ErrorCode`, a message, and a free-form `data` object. Shape is PROTO's call; a minimal one that keeps `ParseError` for the parse path and adds a handler-error type: ```cpp struct HandlerError { ErrorCode code{ErrorCode::InternalError}; std::string message; nlohmann::json data{nullptr}; }; template using HandlerResult = std::expected; // Dispatcher::on_* return HandlerResult; dispatch() forwards code/message/data // straight into make_error() instead of hard-coding InternalError. ``` `FixtureDispatcher` and `conformance_main.cpp` would need the trivial follow-on edit (they only ever return success today, so it is a type-name swap). Until this lands, DAEMON's `rpc/` server layer handles `-3200x` around `dispatch()` where it can, and every genuine in-handler failure collapses to `-32603` with a clear message — visibly non-conformant on three error fixtures, tracked here, not worked around by inventing a side channel. --- ## P2. `SessionHelloResult.transport` and `-32001` `data.expected` — minor clarifications - `session.hello.version-mismatch`'s `data.expected` is `"1.0.0"` in the fixture, i.e. the daemon's *current* protocol version string, not a bare major. DAEMON will echo `kProtocolVersion` (`"1.3.0"`) there unless PROTO wants the fixture's literal `"1.0.0"` preserved — flag if the conformance compare is exact on that field rather than structural. - `SessionHelloResult.transport` is `std::optional` — DAEMON intends to always populate it (`"uds"` / `"ws"`) so a client knows its privilege level up front, as the field's own description invites. No change requested; noting the intent so a later "why is this always set" review has the answer.