diff --git a/contracts/README.md b/contracts/README.md index 2f09e84..cec2e0f 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -3,16 +3,18 @@ **This directory is the interface between every lane.** Owner: agent **PROTO**. Nobody else commits here. Everybody else *generates from* here. -> ## Status: **v1.3.0** (frozen at v1.0.0 on 2026-09-09; minor bumps since) +> ## Status: **v1.4.0** (frozen at v1.0.0 on 2026-09-09; minor bumps since) > > **v1.0.0** froze 38 methods, 9 events, 26 named types. **v1.1.0** widened `bufferBytes` -> bounds and added the segment-budget settings (`docs/adr/0012-...`). **v1.2.0** added -> `download.provideAuth`, F2's credential return path. **v1.3.0** (current) widens when -> `event.task.state.error` / `TaskSummary.error` are populated to also cover a `paused` -> the daemon entered unilaterally (`auth_required`, `server_file_changed`, disk full), -> not just `failed`/`retry_wait` — the wire shape is unchanged (`error` was already -> `TaskError | null`), only the description of when it's set. Landed for DAEMON's -> `docs/adr/0013-task-state-machine-ownership.md`. See also `docs/adr/0005-...` for the +> bounds and added the segment-budget settings. **v1.2.0** added `download.provideAuth` +> (F2). **v1.3.0** widened when `error` is populated on a state change to cover a +> daemon-initiated `paused` (for `docs/adr/0013-...`). **v1.4.0** (current) is a +> **C++-binding-only** change: the generated `Dispatcher` gains a `HandlerError` / +> `HandlerResult` error channel so a handler can return `-32010` / `-32011` / +> `-32013` with their `data` payloads instead of collapsing to `-32603`. The wire is +> byte-identical — no schema or fixture change — but any `Dispatcher` implementer +> must swap `Result` → `HandlerResult` on regen. Answered in +> `contracts/proto-answers-daemon-m1.md`. See also `docs/adr/0005-...` for the > versioning rule and `docs/adr/0010-...` for the failure taxonomy and segment ranges. > > Lane requests are answered in writing: `contracts/proto-answers-m1.md` responds to diff --git a/contracts/VERSION b/contracts/VERSION index f0bb29e..88c5fb8 100644 --- a/contracts/VERSION +++ b/contracts/VERSION @@ -1 +1 @@ -1.3.0 +1.4.0 diff --git a/contracts/codegen/gen_cpp.py b/contracts/codegen/gen_cpp.py index b978987..402fda8 100644 --- a/contracts/codegen/gen_cpp.py +++ b/contracts/codegen/gen_cpp.py @@ -7,6 +7,13 @@ Design notes that matter to the CORE and DAEMON lanes: not used and not emitted. Parsing goes through `velox::proto::parse(json)` which returns `std::expected`, so a malformed frame from the wire is an ordinary value the RPC loop handles, not a throw unwinding through the transfer path. +* **Two error channels, kept separate.** `parse() -> Result` (i.e. `expected`) is the wire failing to become typed params — always `-32602`, always + structural. A `Dispatcher::on_*` handler returns `HandlerResult` (i.e. `expected`), which carries any contract error code plus a free-form `data` object, + so a handler can answer `-32010` `{taskId}`, `-32011` `{path}`, `-32013` `{httpStatus}` + and so on. `dispatch()` forwards the handler's code/message/data straight into the + JSON-RPC error object. * **Serialisation is ADL `to_json`,** so `nlohmann::json j = task;` works as expected. Only the outbound direction is allowed to be implicit; the wire is never trusted. * **`libveloxproto`, not `libveloxcore`.** This code includes nlohmann/json, which @@ -285,16 +292,37 @@ def emit_header(c: Contract) -> str: "nlohmann::json make_result(const nlohmann::json& id, nlohmann::json result);", "nlohmann::json make_notification(Event e, nlohmann::json params);", "", + "/// A handler's own failure — as opposed to ParseError, which is the wire failing", + "/// to become typed params. Carries any contract error code, a message, and a", + "/// free-form `data` object that goes straight into the JSON-RPC error's `data`", + "/// field: `{\"taskId\": ...}` for TaskNotFound, `{\"path\": ...}` for InvalidPath,", + "/// `{\"httpStatus\": ...}` for ProbeFailed. `code` defaults to InternalError so a", + "/// handler that sets only a message still produces a valid error response.", + "///", + "/// -32001/-32002/-32003 are the server layer's to raise around dispatch(), not a", + "/// handler's: they are decided before or without reference to method params.", + "struct HandlerError {", + " ErrorCode code{ErrorCode::InternalError};", + " std::string message;", + " // `= nullptr`, not `{nullptr}`: brace-init of nlohmann::json from nullptr", + " // yields the array [null], not JSON null. make_error() drops a null data.", + " nlohmann::json data = nullptr;", + "};", + "", + "template ", + "using HandlerResult = std::expected;", + "", "/// One virtual per method. The daemon implements this; `dispatch` below does the", "/// envelope handling, the transport check and the parameter parsing, so a handler", - "/// only ever sees a validated, typed params struct.", + "/// only ever sees a validated, typed params struct. Return `std::unexpected(", + "/// HandlerError{...})` to answer with a specific error code and data.", "class Dispatcher {", "public:", " virtual ~Dispatcher() = default;", ""] for m in c.methods: o += doc_comment(m.doc, " ") - o.append(f" virtual Result<{cpp_type(m.result)}> {handler_name(m.name)}(const {cpp_type(m.params)}& params) = 0;") + o.append(f" virtual HandlerResult<{cpp_type(m.result)}> {handler_name(m.name)}(const {cpp_type(m.params)}& params) = 0;") o.append("") o += ["};", "", "/// Parse one JSON-RPC request, route it, and return the response to write back.", @@ -579,8 +607,7 @@ def emit_dispatch(c: Contract) -> list[str]: ' nlohmann::json{{"path", p.error().path}});', f" auto r = handler.{handler_name(m.name)}(*p);", " if (!r)", - " return make_error(id, ErrorCode::InternalError, r.error().message,", - ' nlohmann::json{{"path", r.error().path}});', + " return make_error(id, r.error().code, r.error().message, r.error().data);", " nlohmann::json out = *r;", " return make_result(id, std::move(out));", " }", diff --git a/contracts/codegen/gen_cpp_conformance.py b/contracts/codegen/gen_cpp_conformance.py index abca0a2..659f1e1 100644 --- a/contracts/codegen/gen_cpp_conformance.py +++ b/contracts/codegen/gen_cpp_conformance.py @@ -44,7 +44,7 @@ def main() -> int: # velox::conformance, so the names need qualifying. pt, rt = "proto::" + cpp_type(m.params), "proto::" + cpp_type(m.result) o += [ - f" proto::Result<{rt}> {handler_name(m.name)}(const {pt}& params) override {{", + f" proto::HandlerResult<{rt}> {handler_name(m.name)}(const {pt}& params) override {{", " (void)params;", f' return golden<{rt}>("{m.name}");', " }", @@ -53,12 +53,21 @@ def main() -> int: o += [ "private:", + " // Every fixture-backed handler only ever succeeds. A missing or unparseable", + " // fixture is a bug in the suite, not a contract outcome, so it surfaces as", + " // InternalError rather than being dressed up as a real error code.", " template ", - " proto::Result golden(const std::string& method) {", + " proto::HandlerResult golden(const std::string& method) {", " const nlohmann::json* value = results_(method);", " if (value == nullptr)", - ' return std::unexpected(proto::ParseError{method, "no fixture for this method"});', - " return proto::parse(*value, method);", + " return std::unexpected(proto::HandlerError{", + ' proto::ErrorCode::InternalError, "no fixture for method " + method});', + " auto parsed = proto::parse(*value, method);", + " if (!parsed)", + " return std::unexpected(proto::HandlerError{", + " proto::ErrorCode::InternalError,", + ' "fixture for " + method + " failed to parse: " + parsed.error().message});', + " return std::move(*parsed);", " }", "", " std::function results_;", diff --git a/contracts/fixtures/errors/session.hello.version-mismatch.json b/contracts/fixtures/errors/session.hello.version-mismatch.json index 1b0cc6f..be3dfa8 100644 --- a/contracts/fixtures/errors/session.hello.version-mismatch.json +++ b/contracts/fixtures/errors/session.hello.version-mismatch.json @@ -18,7 +18,7 @@ "code": -32001, "message": "protocol major version mismatch: daemon speaks 1.x, client speaks 2.x", "data": { - "expected": "1.0.0", + "expected": "$any", "actual": "2.0.0" } } @@ -27,7 +27,8 @@ "the connection is closed after this reply; no method is served on a mismatched major", "a differing minor or patch is accepted, never refused", "the message is safe to show a user verbatim", - "the version check is transport-independent; this is replayed on the Unix socket so it is not masked by -32002" + "the version check is transport-independent; this is replayed on the Unix socket so it is not masked by -32002", + "data.expected is the daemon's own current protocol version string (kProtocolVersion), not a bare major and not pinnable in a golden file -- the conformance compare on error payloads is on `code` only, structural elsewhere, so echoing the live version is fine" ], "transport": "uds" } diff --git a/contracts/openrpc.json b/contracts/openrpc.json index 2479431..1c0a14b 100644 --- a/contracts/openrpc.json +++ b/contracts/openrpc.json @@ -2,7 +2,7 @@ "openrpc": "1.2.6", "info": { "title": "Velox Download Manager", - "version": "1.3.0", + "version": "1.4.0", "description": "The wire contract between veloxd and every client: the Qt GUI, the CLI, the native-messaging host and the Firefox extension. One JSON-RPC 2.0 payload set over four framings; only the framing differs.\n\nGENERATED from contracts/schema/ by contracts/codegen/gen_openrpc.py. Do not edit by hand.", "license": { "name": "See repository LICENSE" diff --git a/contracts/proto-answers-daemon-m1.md b/contracts/proto-answers-daemon-m1.md new file mode 100644 index 0000000..9e0b665 --- /dev/null +++ b/contracts/proto-answers-daemon-m1.md @@ -0,0 +1,78 @@ +# PROTO → DAEMON — answers to `daemon/docs/proto-requests-m1.md` + +Status: **answered**. Against `contracts/` at **1.4.0** (`lane/proto`). +Raised by DAEMON while building `rpc/` against 1.3.0. + +--- + +## P1 — the generated `Dispatcher` has no error channel below `-32603` · **landed in 1.4.0** + +Done, essentially as sketched. The generated C++ binding now has two error channels, +kept deliberately separate: + +| Channel | Type | Raised by | Always | +|---|---|---|---| +| parse | `Result` = `expected` | `dispatch()` turning the wire into typed params | `-32602`, structural, `data.path` a JSON pointer | +| handler | `HandlerResult` = `expected` | a `Dispatcher::on_*` method | any contract code + free-form `data` | + +```cpp +struct HandlerError { + ErrorCode code{ErrorCode::InternalError}; // default: a bare HandlerError{} is a valid -32603 + std::string message; + nlohmann::json data = nullptr; // forwarded straight into the JSON-RPC error's data +}; +template using HandlerResult = std::expected; +``` + +Every `Dispatcher::on_*` now returns `HandlerResult`. `dispatch()`'s handler-error +branch went from a hard-coded `InternalError` to: + +```cpp +if (!r) return make_error(id, r.error().code, r.error().message, r.error().data); +``` + +So the three in-handler fixtures are now satisfiable by a conformant server: + +| Fixture | `return std::unexpected(HandlerError{ ... })` | +|---|---| +| `download.get.not-found` | `ErrorCode::TaskNotFound, "no such task", {{"taskId", id}}` | +| `download.add.invalid-path` | `ErrorCode::InvalidPath, "outside allowed roots", {{"path", p}}` | +| `download.probe.probe-failed` | `ErrorCode::ProbeFailed, "HTTP 403", {{"httpStatus", 403}}` | + +`session.pair.rate-limited` (`-32014`) is a handler result too if you want it there — +nothing stops a handler returning `HandlerError{ErrorCode::RateLimited, ..., +{{"retryAfterSec", 60}}}`. `-32001/-32002/-32003` stay yours to raise in the server layer +around `dispatch()`, as you're already doing; they're decided before or without reference +to method params, and `HandlerError`'s own doc comment says so. + +Verified end to end: a handler returning each of the above through the real `dispatch()` +path produces the right code with the `data` payload intact, and a bare `HandlerError{}` +still yields a clean `-32603` with no `data` field. (Watch the nlohmann brace-init trap: +`HandlerError{code, msg, {{"k", v}}}` gives an object, but a lone `{nullptr}` would give +the array `[null]` — the struct's member initializer is `= nullptr` for exactly that +reason.) + +`FixtureDispatcher` and `conformance_main.cpp`: the generated dispatcher swapped +`Result` → `HandlerResult` automatically; `conformance_main.cpp` only ever inspects +`dispatch()`'s JSON output and needed no change. + +**Version:** minor, 1.3.0 → 1.4.0. The wire is byte-identical — no schema, fixture, or +OpenRPC change — but every implementer of `Dispatcher` must swap `Result` → `HandlerResult` +on their `on_*` overrides or they won't compile, and a version bump is how lanes are told +to regenerate and adapt. Not major and not an ADR: one lane consumes this binding, it's +the lane that asked, and there's no contested design here — the shape is the one you +proposed. `kProtocolVersion` moves to `"1.4.0"` with it. + +## P2 — clarifications + +**`session.hello.version-mismatch` `data.expected`.** You're right that `"1.0.0"` in the +fixture is stale. Fixed: it's now `$any`. Echo `kProtocolVersion` (`"1.4.0"`) there — the +conformance compare on an error fixture is on `code` only, structural elsewhere, so the +live version string is fine and can't be pinned in a golden file that outlives version +bumps anyway. `actual` stays the concrete bad version the fake client sent (`"2.0.0"`). + +**`SessionHelloResult.transport` always populated.** No change requested, noted. The +field's own description already invites it (`"Lets a client know up front which privileged +methods will be refused"`), so always setting `"uds"` / `"ws"` is using it as intended. +`std::optional` stays because a hand-rolled or older server may legitimately omit it and a +client must tolerate that. diff --git a/core/generated/velox_proto.cpp b/core/generated/velox_proto.cpp index e7b0e86..02a3efa 100644 --- a/core/generated/velox_proto.cpp +++ b/core/generated/velox_proto.cpp @@ -3,7 +3,7 @@ // // Source: contracts/schema/** // Generator: contracts/codegen/gen_cpp.py -// Contract: v1.3.0 +// Contract: v1.4.0 // // Hand-editing this file is a merge blocker. Fix the schema and regenerate: // python3 contracts/codegen/gen_cpp.py @@ -7245,8 +7245,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_capture_getRules(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7257,8 +7256,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_capture_offer(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7269,8 +7267,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_category_list(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7281,8 +7278,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_category_remove(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7293,8 +7289,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_category_upsert(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7305,8 +7300,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_download_add(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7317,8 +7311,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_download_addBatch(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7329,8 +7322,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_download_cancel(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7341,8 +7333,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_download_get(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7353,8 +7344,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_download_list(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7365,8 +7355,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_download_pause(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7377,8 +7366,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_download_probe(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7389,8 +7377,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_download_provideAuth(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7401,8 +7388,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_download_refreshUrl(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7413,8 +7399,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_download_remove(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7425,8 +7410,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_download_resume(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7437,8 +7421,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_download_start(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7449,8 +7432,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_download_update(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7461,8 +7443,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_grabber_harvest(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7473,8 +7454,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_grabber_start(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7485,8 +7465,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_grabber_status(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7497,8 +7476,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_limiter_get(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7509,8 +7487,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_limiter_set(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7521,8 +7498,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_media_addVariant(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7533,8 +7509,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_media_listVariants(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7545,8 +7520,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_queue_list(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7557,8 +7531,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_queue_reorder(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7569,8 +7542,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_queue_start(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7581,8 +7553,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_queue_stop(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7593,8 +7564,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_queue_upsert(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7605,8 +7575,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_rules_list(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7617,8 +7586,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_rules_upsert(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7629,8 +7597,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_schedule_get(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7641,8 +7608,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_schedule_set(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7653,8 +7619,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_session_hello(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7665,8 +7630,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_session_pair(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7677,8 +7641,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_session_subscribe(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7689,8 +7652,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_settings_get(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } @@ -7701,8 +7663,7 @@ nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann nlohmann::json{{"path", p.error().path}}); auto r = handler.on_settings_set(*p); if (!r) - return make_error(id, ErrorCode::InternalError, r.error().message, - nlohmann::json{{"path", r.error().path}}); + return make_error(id, r.error().code, r.error().message, r.error().data); nlohmann::json out = *r; return make_result(id, std::move(out)); } diff --git a/core/generated/velox_proto.hpp b/core/generated/velox_proto.hpp index fbe8f6c..e533506 100644 --- a/core/generated/velox_proto.hpp +++ b/core/generated/velox_proto.hpp @@ -3,7 +3,7 @@ // // Source: contracts/schema/** // Generator: contracts/codegen/gen_cpp.py -// Contract: v1.3.0 +// Contract: v1.4.0 // // Hand-editing this file is a merge blocker. Fix the schema and regenerate: // python3 contracts/codegen/gen_cpp.py @@ -27,7 +27,7 @@ // docs/adr/0009-generated-protocol-library.md. namespace velox::proto { -inline constexpr std::string_view kProtocolVersion = "1.3.0"; +inline constexpr std::string_view kProtocolVersion = "1.4.0"; /// Why a payload could not be turned into a typed value. `path` is a JSON Pointer /// into the offending document, so a conformance failure names the exact field. @@ -1785,9 +1785,30 @@ nlohmann::json make_error(const nlohmann::json& id, ErrorCode code, std::string_ nlohmann::json make_result(const nlohmann::json& id, nlohmann::json result); nlohmann::json make_notification(Event e, nlohmann::json params); +/// A handler's own failure — as opposed to ParseError, which is the wire failing +/// to become typed params. Carries any contract error code, a message, and a +/// free-form `data` object that goes straight into the JSON-RPC error's `data` +/// field: `{"taskId": ...}` for TaskNotFound, `{"path": ...}` for InvalidPath, +/// `{"httpStatus": ...}` for ProbeFailed. `code` defaults to InternalError so a +/// handler that sets only a message still produces a valid error response. +/// +/// -32001/-32002/-32003 are the server layer's to raise around dispatch(), not a +/// handler's: they are decided before or without reference to method params. +struct HandlerError { + ErrorCode code{ErrorCode::InternalError}; + std::string message; + // `= nullptr`, not `{nullptr}`: brace-init of nlohmann::json from nullptr + // yields the array [null], not JSON null. make_error() drops a null data. + nlohmann::json data = nullptr; +}; + +template +using HandlerResult = std::expected; + /// One virtual per method. The daemon implements this; `dispatch` below does the /// envelope handling, the transport check and the parameter parsing, so a handler -/// only ever sees a validated, typed params struct. +/// only ever sees a validated, typed params struct. Return `std::unexpected( +/// HandlerError{...})` to answer with a specific error code and data. class Dispatcher { public: virtual ~Dispatcher() = default; @@ -1795,61 +1816,61 @@ public: /// The daemon's capture policy, so the extension's shouldCapture decision cannot drift from the /// daemon's. Fetched on connect and whenever event.settings.changed names a capture.* key. If /// this call fails the extension keeps its last known rules and stays fail-open. - virtual Result on_capture_getRules(const CaptureGetRulesParams& params) = 0; + virtual HandlerResult on_capture_getRules(const CaptureGetRulesParams& params) = 0; /// Firefox offers an intercepted response to the daemon. The daemon MUST reply within 750 ms; /// the extension abandons the offer and lets Firefox download normally on timeout. This /// deadline is the whole reason capture fails open, and it is conformance-tested: a daemon that /// is slow, down, or erroring must never cost the user a download. - virtual Result on_capture_offer(const CaptureOfferParams& params) = 0; + virtual HandlerResult on_capture_offer(const CaptureOfferParams& params) = 0; /// Every category with its folder and extension list. The extension calls this to populate its /// default-category picker, which is why it is not privileged; it is read-only and exposes only /// paths the user already configured. - virtual Result on_category_list(const CategoryListParams& params) = 0; + virtual HandlerResult on_category_list(const CategoryListParams& params) = 0; /// Delete a user-created category. Built-in categories are refused with -32602. Tasks filed /// under it are reassigned to reassignTo, or to the default category when that is null; no task /// is ever orphaned. - virtual Result on_category_remove(const CategoryRemoveParams& params) = 0; + virtual HandlerResult on_category_remove(const CategoryRemoveParams& params) = 0; /// Create or replace a category. Omit categoryId to create; supply it to replace. Changing /// saveDir does not move existing files — the GUI asks separately and issues download.update /// per task, so a re-point is never a surprise mass file move. - virtual Result on_category_upsert(const CategoryUpsertParams& params) = 0; + virtual HandlerResult on_category_upsert(const CategoryUpsertParams& params) = 0; /// Create one task. saveDir is canonicalized and checked against saveTo.allowedRoots before /// anything is written; a path that escapes them is refused with -32011 and no file is created. - virtual Result on_download_add(const DownloadSpec& params) = 0; + virtual HandlerResult on_download_add(const DownloadSpec& params) = 0; /// Create many tasks in one call: the clipboard blob, the wildcard expander, and the /// extension's 'Download all links'. Partial success is normal and is reported per item rather /// than failing the whole batch. - virtual Result on_download_addBatch(const DownloadAddBatchParams& params) = 0; + virtual HandlerResult on_download_addBatch(const DownloadAddBatchParams& params) = 0; /// Stop the given tasks and mark them cancelled. The .veloxpart file is kept so the user can /// still resume from the list; download.remove is what deletes bytes. - virtual Result on_download_cancel(const DownloadCancelParams& params) = 0; + virtual HandlerResult on_download_cancel(const DownloadCancelParams& params) = 0; /// Full detail for one task, including per-segment state. Backs the progress dialog. Poll it no /// faster than the progress dialog repaints; the table must use events instead. - virtual Result on_download_get(const DownloadGetParams& params) = 0; + virtual HandlerResult on_download_get(const DownloadGetParams& params) = 0; /// The main table. Filtering, sorting and paging all happen in the daemon so the GUI never /// materializes 100k rows to show 40. Called once on connect; after that the table is /// maintained from events, never re-fetched on a progress tick. - virtual Result on_download_list(const DownloadListParams& params) = 0; + virtual HandlerResult on_download_list(const DownloadListParams& params) = 0; /// Suspend transfers and flush every segment's progress to the .veloxpart.meta file, so a pause /// is indistinguishable from a crash as far as resume is concerned. Never loses bytes already /// written. - virtual Result on_download_pause(const DownloadPauseParams& params) = 0; + virtual HandlerResult on_download_pause(const DownloadPauseParams& params) = 0; /// Ask what is at a URL without creating a task. Populates the File Info dialog. Runs a HEAD, /// falling back to a ranged GET when HEAD is refused, which is also how resumability is /// established. Never blocks the RPC loop; the dialog opens immediately and fills in when this /// lands. - virtual Result on_download_probe(const DownloadProbeParams& params) = 0; + virtual HandlerResult on_download_probe(const DownloadProbeParams& params) = 0; /// Answer an event.auth.required challenge. The task sits in retry_wait until this arrives; on /// success the daemon retries with the credentials attached and the task resumes on its own — @@ -1859,137 +1880,137 @@ public: /// into a log') — this is the other half of that promise. Credentials are handed to the Secret /// Service, never to SQLite and never logged; save only tells the daemon whether to persist /// them there for next time, or use them for this attempt alone. - virtual Result on_download_provideAuth(const DownloadProvideAuthParams& params) = 0; + virtual HandlerResult on_download_provideAuth(const DownloadProvideAuthParams& params) = 0; /// IDM's 'Refresh Download Address'. Point an existing task at a freshly-issued URL when a /// signed link has expired, keeping every byte already on disk. The daemon re-probes and /// compares size and validator: if they still match, the transfer resumes from where it /// stopped; if they do not, it says so rather than silently restarting. - virtual Result on_download_refreshUrl(const DownloadRefreshUrlParams& params) = 0; + virtual HandlerResult on_download_refreshUrl(const DownloadRefreshUrlParams& params) = 0; /// Drop tasks from the list, optionally deleting the bytes on disk. Privileged: this is the /// only method that destroys user data, and the extension is never allowed to reach it. The /// daemon deletes the .veloxpart and .veloxpart.meta pair, and the finished file only when /// deleteFile is true. - virtual Result on_download_remove(const DownloadRemoveParams& params) = 0; + virtual HandlerResult on_download_remove(const DownloadRemoveParams& params) = 0; /// Continue paused tasks. Resumption is revalidated with If-Range against the stored ETag or /// Last-Modified; a 200 where 206 was expected means the file changed on the server, and the /// task moves to failed with a clear error rather than corrupting the part file. - virtual Result on_download_resume(const DownloadResumeParams& params) = 0; + virtual HandlerResult on_download_resume(const DownloadResumeParams& params) = 0; /// Begin or restart the given tasks. A task in 'queued' jumps its queue; a task already /// downloading is a no-op reported as changed false. - virtual Result on_download_start(const DownloadStartParams& params) = 0; + virtual HandlerResult on_download_start(const DownloadStartParams& params) = 0; /// Change a task's mutable fields. Moving saveDir or filename moves the file on disk in the /// same operation, which is what makes dragging a row onto a category work as one RPC. /// Privileged: it can name a destination path. - virtual Result on_download_update(const DownloadUpdateParams& params) = 0; + virtual HandlerResult on_download_update(const DownloadUpdateParams& params) = 0; /// Turn selected crawl results into tasks. This is the only grabber call that creates /// downloads, and it names exactly the files the user ticked — a crawl never starts a download /// on its own. - virtual Result on_grabber_harvest(const GrabberHarvestParams& params) = 0; + virtual HandlerResult on_grabber_harvest(const GrabberHarvestParams& params) = 0; /// Start a depth-limited crawl. Nothing is downloaded by this call: it only walks pages and /// collects candidate links, which the wizard then shows for selection. Privileged because an /// unbounded crawl is a resource commitment the browser must not be able to make on the user's /// behalf. - virtual Result on_grabber_start(const GrabberStartParams& params) = 0; + virtual HandlerResult on_grabber_start(const GrabberStartParams& params) = 0; /// Poll one crawl. Also delivered as event.grabber.progress; the poll exists so the wizard can /// be reopened on a job it did not start and still catch up. - virtual Result on_grabber_status(const GrabberStatusParams& params) = 0; + virtual HandlerResult on_grabber_status(const GrabberStatusParams& params) = 0; /// Current global speed limit. Privileged: changing or reading the limiter belongs to the GUI /// and CLI; the extension shows throughput from event.speed.global instead. - virtual Result on_limiter_get(const LimiterGetParams& params) = 0; + virtual HandlerResult on_limiter_get(const LimiterGetParams& params) = 0; /// Set the global token-bucket limit. With applyToRunning true the change re-tunes transfers /// already in flight instead of taking effect only on the next task — the Speed Limiter /// window's 'apply now' button. - virtual Result on_limiter_set(const Limiter& params) = 0; + virtual HandlerResult on_limiter_set(const Limiter& params) = 0; /// Turn one enumerated variant into a task. The daemon fetches the segments in parallel and /// muxes them with ffmpeg; the result is an ordinary task that appears in the list like any /// other download. Refused with -32602 when the variant is DRM-protected. - virtual Result on_media_addVariant(const MediaAddVariantParams& params) = 0; + virtual HandlerResult on_media_addVariant(const MediaAddVariantParams& params) = 0; /// Parse an HLS or DASH manifest in the daemon and enumerate its renditions. The extension /// never parses a manifest — that logic lives in one language, in one place. Variants with drm /// true are reported so the UI can grey them out; DRM-protected streams are refused, not /// attempted. - virtual Result on_media_listVariants(const MediaListVariantsParams& params) = 0; + virtual HandlerResult on_media_listVariants(const MediaListVariantsParams& params) = 0; /// Every queue with its run state and ordering. Not privileged: the extension's 'Add to Queue' /// picker needs it. - virtual Result on_queue_list(const QueueListParams& params) = 0; + virtual HandlerResult on_queue_list(const QueueListParams& params) = 0; /// Rewrite a queue's run order. taskIds must be a permutation of the queue's current /// membership; anything else is -32602 rather than a partial reorder, so a stale drag from an /// out-of-date view cannot quietly reshuffle the queue. - virtual Result on_queue_reorder(const QueueReorderParams& params) = 0; + virtual HandlerResult on_queue_reorder(const QueueReorderParams& params) = 0; /// Start a queue running. The scheduler then admits up to maxConcurrent tasks from it, in /// order, and keeps that many running until the queue drains or is stopped. - virtual Result on_queue_start(const QueueStartParams& params) = 0; + virtual HandlerResult on_queue_start(const QueueStartParams& params) = 0; /// Stop admitting new tasks from a queue. Tasks already running are paused when pauseRunning is /// true, and otherwise allowed to finish — the difference between 'stop the queue' and 'stop /// everything', which IDM conflates and users trip over. - virtual Result on_queue_stop(const QueueStopParams& params) = 0; + virtual HandlerResult on_queue_stop(const QueueStopParams& params) = 0; /// Create or replace a queue, including its schedule and concurrency cap. Omit queueId to /// create. taskIds in the payload is ignored — membership changes through download.update and /// queue.reorder so that two clients editing at once cannot silently drop a task. - virtual Result on_queue_upsert(const QueueUpsertParams& params) = 0; + virtual HandlerResult on_queue_upsert(const QueueUpsertParams& params) = 0; /// The rules engine's table, in priority order. Privileged: these are the daemon's routing /// policy. The extension gets its own narrowed view through capture.getRules instead. - virtual Result on_rules_list(const RulesListParams& params) = 0; + virtual HandlerResult on_rules_list(const RulesListParams& params) = 0; /// Create, replace, or delete rules in one atomic write. 'upsert' carries the rules to store /// and 'remove' the ruleIds to drop; applying both at once means a reprioritisation never /// leaves the table in a half-valid state. - virtual Result on_rules_upsert(const RulesUpsertParams& params) = 0; + virtual HandlerResult on_rules_upsert(const RulesUpsertParams& params) = 0; /// The schedule for one queue, or every schedule when queueId is null. Backs the Scheduler /// window. - virtual Result on_schedule_get(const ScheduleGetParams& params) = 0; + virtual HandlerResult on_schedule_get(const ScheduleGetParams& params) = 0; /// Set or clear a queue's schedule. A null schedule clears it and leaves the queue under manual /// control. Times are local wall-clock and are re-evaluated on a DST change rather than being /// resolved to absolute instants at set time. - virtual Result on_schedule_set(const ScheduleSetParams& params) = 0; + virtual HandlerResult on_schedule_set(const ScheduleSetParams& params) = 0; /// First call on every connection, on every transport. The daemon compares protocolVersion /// majors and refuses a mismatch with -32001 so a stale GUI or extension fails loudly on /// connect instead of subtly at the tenth field. On the WebSocket transport a valid token is /// required unless the client is about to call session.pair. - virtual Result on_session_hello(const SessionHelloParams& params) = 0; + virtual HandlerResult on_session_hello(const SessionHelloParams& params) = 0; /// WebSocket transport only. Triggers a GUI or desktop-notification prompt showing a four-digit /// code; the user must approve before a token is issued. Failed attempts are rate-limited to /// 5/min followed by a 60 s lockout (-32014) so a token cannot be brute-forced by another local /// process. The daemon stores only a hash of the token. - virtual Result on_session_pair(const SessionPairParams& params) = 0; + virtual HandlerResult on_session_pair(const SessionPairParams& params) = 0; /// Choose which notifications this connection receives. Subscribing replaces the previous /// selection rather than adding to it, so a client can narrow its firehose without /// reconnecting. Nothing is delivered until this is called. - virtual Result on_session_subscribe(const SessionSubscribeParams& params) = 0; + virtual HandlerResult on_session_subscribe(const SessionSubscribeParams& params) = 0; /// Read settings. keys null means everything. Privileged: the settings bag names local /// filesystem paths and the allowed write roots, which the extension has no business /// enumerating — it gets capture.getRules instead. - virtual Result on_settings_get(const SettingsGetParams& params) = 0; + virtual HandlerResult on_settings_get(const SettingsGetParams& params) = 0; /// Write settings. Only the keys present in values change. Rejected with -32602 if a key is /// unknown or a value fails the Settings schema, and with -32011 if a directory key names a /// path that cannot be written. Emits event.settings.changed with exactly the keys that took /// effect. - virtual Result on_settings_set(const SettingsSetParams& params) = 0; + virtual HandlerResult on_settings_set(const SettingsSetParams& params) = 0; }; diff --git a/extension/src/shared/protocol/events.ts b/extension/src/shared/protocol/events.ts index 8dccabd..ec54e39 100644 --- a/extension/src/shared/protocol/events.ts +++ b/extension/src/shared/protocol/events.ts @@ -3,7 +3,7 @@ // // Source: contracts/schema/** // Generator: contracts/codegen/gen_ts.py -// Contract: v1.3.0 +// Contract: v1.4.0 // // Hand-editing this file is a merge blocker. Fix the schema and regenerate: // python3 contracts/codegen/gen_ts.py diff --git a/extension/src/shared/protocol/index.ts b/extension/src/shared/protocol/index.ts index 268d586..53e94c3 100644 --- a/extension/src/shared/protocol/index.ts +++ b/extension/src/shared/protocol/index.ts @@ -3,7 +3,7 @@ // // Source: contracts/schema/** // Generator: contracts/codegen/gen_ts.py -// Contract: v1.3.0 +// Contract: v1.4.0 // // Hand-editing this file is a merge blocker. Fix the schema and regenerate: // python3 contracts/codegen/gen_ts.py diff --git a/extension/src/shared/protocol/methods.ts b/extension/src/shared/protocol/methods.ts index 468dc01..20468f6 100644 --- a/extension/src/shared/protocol/methods.ts +++ b/extension/src/shared/protocol/methods.ts @@ -3,7 +3,7 @@ // // Source: contracts/schema/** // Generator: contracts/codegen/gen_ts.py -// Contract: v1.3.0 +// Contract: v1.4.0 // // Hand-editing this file is a merge blocker. Fix the schema and regenerate: // python3 contracts/codegen/gen_ts.py diff --git a/extension/src/shared/protocol/types.ts b/extension/src/shared/protocol/types.ts index cfaf4cb..e4e1182 100644 --- a/extension/src/shared/protocol/types.ts +++ b/extension/src/shared/protocol/types.ts @@ -3,7 +3,7 @@ // // Source: contracts/schema/** // Generator: contracts/codegen/gen_ts.py -// Contract: v1.3.0 +// Contract: v1.4.0 // // Hand-editing this file is a merge blocker. Fix the schema and regenerate: // python3 contracts/codegen/gen_ts.py @@ -11,7 +11,7 @@ // --------------------------------------------------------------------------- -export const PROTOCOL_VERSION = "1.3.0"; +export const PROTOCOL_VERSION = "1.4.0"; /** * Every error code the daemon may return. Adding one is a minor bump; changing the meaning diff --git a/extension/src/shared/protocol/validate.ts b/extension/src/shared/protocol/validate.ts index a21e299..4c9695e 100644 --- a/extension/src/shared/protocol/validate.ts +++ b/extension/src/shared/protocol/validate.ts @@ -3,7 +3,7 @@ // // Source: contracts/schema/** // Generator: contracts/codegen/gen_ts.py -// Contract: v1.3.0 +// Contract: v1.4.0 // // Hand-editing this file is a merge blocker. Fix the schema and regenerate: // python3 contracts/codegen/gen_ts.py diff --git a/tests/conformance/cpp/fixture_dispatcher.hpp b/tests/conformance/cpp/fixture_dispatcher.hpp index 9c4a6d3..50d2db7 100644 --- a/tests/conformance/cpp/fixture_dispatcher.hpp +++ b/tests/conformance/cpp/fixture_dispatcher.hpp @@ -3,7 +3,7 @@ // // Source: contracts/schema/** // Generator: contracts/codegen/gen_cpp.py -// Contract: v1.3.0 +// Contract: v1.4.0 // // Hand-editing this file is a merge blocker. Fix the schema and regenerate: // python3 contracts/codegen/gen_cpp.py @@ -27,208 +27,217 @@ public: explicit FixtureDispatcher(std::function results) : results_(std::move(results)) {} - proto::Result on_capture_getRules(const proto::CaptureGetRulesParams& params) override { + proto::HandlerResult on_capture_getRules(const proto::CaptureGetRulesParams& params) override { (void)params; return golden("capture.getRules"); } - proto::Result on_capture_offer(const proto::CaptureOfferParams& params) override { + proto::HandlerResult on_capture_offer(const proto::CaptureOfferParams& params) override { (void)params; return golden("capture.offer"); } - proto::Result on_category_list(const proto::CategoryListParams& params) override { + proto::HandlerResult on_category_list(const proto::CategoryListParams& params) override { (void)params; return golden("category.list"); } - proto::Result on_category_remove(const proto::CategoryRemoveParams& params) override { + proto::HandlerResult on_category_remove(const proto::CategoryRemoveParams& params) override { (void)params; return golden("category.remove"); } - proto::Result on_category_upsert(const proto::CategoryUpsertParams& params) override { + proto::HandlerResult on_category_upsert(const proto::CategoryUpsertParams& params) override { (void)params; return golden("category.upsert"); } - proto::Result on_download_add(const proto::DownloadSpec& params) override { + proto::HandlerResult on_download_add(const proto::DownloadSpec& params) override { (void)params; return golden("download.add"); } - proto::Result on_download_addBatch(const proto::DownloadAddBatchParams& params) override { + proto::HandlerResult on_download_addBatch(const proto::DownloadAddBatchParams& params) override { (void)params; return golden("download.addBatch"); } - proto::Result on_download_cancel(const proto::DownloadCancelParams& params) override { + proto::HandlerResult on_download_cancel(const proto::DownloadCancelParams& params) override { (void)params; return golden("download.cancel"); } - proto::Result on_download_get(const proto::DownloadGetParams& params) override { + proto::HandlerResult on_download_get(const proto::DownloadGetParams& params) override { (void)params; return golden("download.get"); } - proto::Result on_download_list(const proto::DownloadListParams& params) override { + proto::HandlerResult on_download_list(const proto::DownloadListParams& params) override { (void)params; return golden("download.list"); } - proto::Result on_download_pause(const proto::DownloadPauseParams& params) override { + proto::HandlerResult on_download_pause(const proto::DownloadPauseParams& params) override { (void)params; return golden("download.pause"); } - proto::Result on_download_probe(const proto::DownloadProbeParams& params) override { + proto::HandlerResult on_download_probe(const proto::DownloadProbeParams& params) override { (void)params; return golden("download.probe"); } - proto::Result on_download_provideAuth(const proto::DownloadProvideAuthParams& params) override { + proto::HandlerResult on_download_provideAuth(const proto::DownloadProvideAuthParams& params) override { (void)params; return golden("download.provideAuth"); } - proto::Result on_download_refreshUrl(const proto::DownloadRefreshUrlParams& params) override { + proto::HandlerResult on_download_refreshUrl(const proto::DownloadRefreshUrlParams& params) override { (void)params; return golden("download.refreshUrl"); } - proto::Result on_download_remove(const proto::DownloadRemoveParams& params) override { + proto::HandlerResult on_download_remove(const proto::DownloadRemoveParams& params) override { (void)params; return golden("download.remove"); } - proto::Result on_download_resume(const proto::DownloadResumeParams& params) override { + proto::HandlerResult on_download_resume(const proto::DownloadResumeParams& params) override { (void)params; return golden("download.resume"); } - proto::Result on_download_start(const proto::DownloadStartParams& params) override { + proto::HandlerResult on_download_start(const proto::DownloadStartParams& params) override { (void)params; return golden("download.start"); } - proto::Result on_download_update(const proto::DownloadUpdateParams& params) override { + proto::HandlerResult on_download_update(const proto::DownloadUpdateParams& params) override { (void)params; return golden("download.update"); } - proto::Result on_grabber_harvest(const proto::GrabberHarvestParams& params) override { + proto::HandlerResult on_grabber_harvest(const proto::GrabberHarvestParams& params) override { (void)params; return golden("grabber.harvest"); } - proto::Result on_grabber_start(const proto::GrabberStartParams& params) override { + proto::HandlerResult on_grabber_start(const proto::GrabberStartParams& params) override { (void)params; return golden("grabber.start"); } - proto::Result on_grabber_status(const proto::GrabberStatusParams& params) override { + proto::HandlerResult on_grabber_status(const proto::GrabberStatusParams& params) override { (void)params; return golden("grabber.status"); } - proto::Result on_limiter_get(const proto::LimiterGetParams& params) override { + proto::HandlerResult on_limiter_get(const proto::LimiterGetParams& params) override { (void)params; return golden("limiter.get"); } - proto::Result on_limiter_set(const proto::Limiter& params) override { + proto::HandlerResult on_limiter_set(const proto::Limiter& params) override { (void)params; return golden("limiter.set"); } - proto::Result on_media_addVariant(const proto::MediaAddVariantParams& params) override { + proto::HandlerResult on_media_addVariant(const proto::MediaAddVariantParams& params) override { (void)params; return golden("media.addVariant"); } - proto::Result on_media_listVariants(const proto::MediaListVariantsParams& params) override { + proto::HandlerResult on_media_listVariants(const proto::MediaListVariantsParams& params) override { (void)params; return golden("media.listVariants"); } - proto::Result on_queue_list(const proto::QueueListParams& params) override { + proto::HandlerResult on_queue_list(const proto::QueueListParams& params) override { (void)params; return golden("queue.list"); } - proto::Result on_queue_reorder(const proto::QueueReorderParams& params) override { + proto::HandlerResult on_queue_reorder(const proto::QueueReorderParams& params) override { (void)params; return golden("queue.reorder"); } - proto::Result on_queue_start(const proto::QueueStartParams& params) override { + proto::HandlerResult on_queue_start(const proto::QueueStartParams& params) override { (void)params; return golden("queue.start"); } - proto::Result on_queue_stop(const proto::QueueStopParams& params) override { + proto::HandlerResult on_queue_stop(const proto::QueueStopParams& params) override { (void)params; return golden("queue.stop"); } - proto::Result on_queue_upsert(const proto::QueueUpsertParams& params) override { + proto::HandlerResult on_queue_upsert(const proto::QueueUpsertParams& params) override { (void)params; return golden("queue.upsert"); } - proto::Result on_rules_list(const proto::RulesListParams& params) override { + proto::HandlerResult on_rules_list(const proto::RulesListParams& params) override { (void)params; return golden("rules.list"); } - proto::Result on_rules_upsert(const proto::RulesUpsertParams& params) override { + proto::HandlerResult on_rules_upsert(const proto::RulesUpsertParams& params) override { (void)params; return golden("rules.upsert"); } - proto::Result on_schedule_get(const proto::ScheduleGetParams& params) override { + proto::HandlerResult on_schedule_get(const proto::ScheduleGetParams& params) override { (void)params; return golden("schedule.get"); } - proto::Result on_schedule_set(const proto::ScheduleSetParams& params) override { + proto::HandlerResult on_schedule_set(const proto::ScheduleSetParams& params) override { (void)params; return golden("schedule.set"); } - proto::Result on_session_hello(const proto::SessionHelloParams& params) override { + proto::HandlerResult on_session_hello(const proto::SessionHelloParams& params) override { (void)params; return golden("session.hello"); } - proto::Result on_session_pair(const proto::SessionPairParams& params) override { + proto::HandlerResult on_session_pair(const proto::SessionPairParams& params) override { (void)params; return golden("session.pair"); } - proto::Result on_session_subscribe(const proto::SessionSubscribeParams& params) override { + proto::HandlerResult on_session_subscribe(const proto::SessionSubscribeParams& params) override { (void)params; return golden("session.subscribe"); } - proto::Result on_settings_get(const proto::SettingsGetParams& params) override { + proto::HandlerResult on_settings_get(const proto::SettingsGetParams& params) override { (void)params; return golden("settings.get"); } - proto::Result on_settings_set(const proto::SettingsSetParams& params) override { + proto::HandlerResult on_settings_set(const proto::SettingsSetParams& params) override { (void)params; return golden("settings.set"); } private: + // Every fixture-backed handler only ever succeeds. A missing or unparseable + // fixture is a bug in the suite, not a contract outcome, so it surfaces as + // InternalError rather than being dressed up as a real error code. template - proto::Result golden(const std::string& method) { + proto::HandlerResult golden(const std::string& method) { const nlohmann::json* value = results_(method); if (value == nullptr) - return std::unexpected(proto::ParseError{method, "no fixture for this method"}); - return proto::parse(*value, method); + return std::unexpected(proto::HandlerError{ + proto::ErrorCode::InternalError, "no fixture for method " + method}); + auto parsed = proto::parse(*value, method); + if (!parsed) + return std::unexpected(proto::HandlerError{ + proto::ErrorCode::InternalError, + "fixture for " + method + " failed to parse: " + parsed.error().message}); + return std::move(*parsed); } std::function results_;