From e28a9667f46f858401d05a9d90ef1711439f840b Mon Sep 17 00:00:00 2001 From: arena-agent Date: Tue, 22 Sep 2026 13:24:03 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix(ci):=20green=20the=20PR=20=E2=80=94=20a?= =?UTF-8?q?rbiter=20stdin=20EPIPE=20race,=20Elixir=20toolchain,=20audits,?= =?UTF-8?q?=20dogfood?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - contract/arbiter.rs: treat BrokenPipe on the stdin write as classifiable (fast/exiting arbiters close the read end before the write lands; the read path already maps that to Closed/Malformed). Previously raced on loaded CI runners: Transport instead of Closed/Malformed. Assertions now print the actual error variant. - arbiter: add .formatter.exs (estate `mix format` gate), commit mix.lock, fix map keyword-ordering syntax, fix config/runtime.exs case-in-keyword compile error, prefix unused vars, mirror audit history to the persisted wire shape (string keys, no atom/string papercut), correct rotation test arithmetic. Installed a local OTP-27/Elixir-1.18 toolchain: mix format clean, 28/28 ExUnit green, escript build + both protocol smokes verified by hand. Ignore the built escript artifact. - ci(dogfood): skip vendored upstream k9 contractiles via the validator's sanctioned INPUT_PATHS_IGNORE (documented in vendor VENDOR.adoc). - audit: .cargo/audit.toml exceptions for four advisories confined to the optional nickel closure (imbl-sized-chunks RUSTSEC-2026-0292, bitmaps x2, paste); documented reachability rationale + revisit point. - docs(UPSTREAM-DELIVERY): mark mix.lock/nickel-native/real-inference as verified done. --- .cargo/audit.toml | 25 ++++++++++++ .github/workflows/dogfood-gate.yml | 5 +++ .gitignore | 2 + docs/UPSTREAM-DELIVERY.adoc | 15 ++++--- src/arbiter/.formatter.exs | 6 +++ src/arbiter/config/runtime.exs | 24 ++++++----- .../lib/conative_gating/arbiter_protocol.ex | 2 +- src/arbiter/lib/conative_gating/audit_log.ex | 7 +++- .../lib/conative_gating/consensus_arbiter.ex | 27 +++++++------ src/arbiter/mix.exs | 5 ++- src/arbiter/mix.lock | 4 ++ src/arbiter/test/arbiter_protocol_test.exs | 2 + src/arbiter/test/audit_log_test.exs | 33 +++++++++++---- src/arbiter/test/protocol_server_test.exs | 22 +++++++--- src/contract/src/arbiter.rs | 40 ++++++++++++++----- vendor/bunsenite/VENDOR.adoc | 8 ++++ 16 files changed, 170 insertions(+), 57 deletions(-) create mode 100644 .cargo/audit.toml create mode 100644 src/arbiter/.formatter.exs create mode 100644 src/arbiter/mix.lock diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 0000000..96672cc --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: MPL-2.0 +# cargo-audit exceptions. +# +# All four advisories are inside the OPTIONAL `nickel` feature's dependency +# closure (nickel-lang-core -> im/imbl stack). None are reachable from the +# default (non-feature) build of this workspace: the `nickel` backend runs a +# pure Nickel evaluator over trusted policy files with no adversarial +# Drop/panic payloads. Fixes require upstream nickel-lang-core to move to a +# newer im/imbl stack. Revisit with the nickel-lang-core 0.19.x upgrade +# (tracked in docs/UPSTREAM-DELIVERY.adoc). +[advisories] +ignore = [ + # imbl-sized-chunks 0.1.3 (pinned by imbl 5.x via nickel-lang-core): + # double-free requires a panicking Drop inside Chunk removal methods; + # the 0.2.x fix is incompatible with imbl's 0.1 requirement. + "RUSTSEC-2026-0292", + # bitmaps 3.2.1 unmaintained (im/imbl transitive). + "RUSTSEC-2026-0247", + # bitmaps 3.2.1 unsound `Bitmap::try_from(&[u8])` — that constructor is + # not exercised by the Nickel evaluator path. + "RUSTSEC-2025-0167", + # paste 1.0.15 unmaintained (nickel-lang-core macro expansion); no + # maintained replacement exists. + "RUSTSEC-2024-0436", +] diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 7dc6bc5..27230ad 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -74,6 +74,11 @@ jobs: fi - name: Validate K9 contracts if: steps.detect.outputs.k9_count > 0 + env: + # The vendored Bunsenite tree ships upstream k9 contractiles that + # are curated in that repo, not here; vendored code is validated + # upstream (see vendor/bunsenite/VENDOR.adoc). + INPUT_PATHS_IGNORE: "/vendor/bunsenite/" run: bash .github/hooks/validate-k9.sh - name: Write summary run: | diff --git a/.gitignore b/.gitignore index 2294218..24a58bd 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,8 @@ Thumbs.db /doc/ *.ez erl_crash.dump +# Built OTP arbiter escript artifact (rebuilt by `mix escript.build`) +/src/arbiter/conative_arbiter # Julia *.jl.cov diff --git a/docs/UPSTREAM-DELIVERY.adoc b/docs/UPSTREAM-DELIVERY.adoc index c4c0482..69f84aa 100644 --- a/docs/UPSTREAM-DELIVERY.adoc +++ b/docs/UPSTREAM-DELIVERY.adoc @@ -45,17 +45,22 @@ State of the feature set delivered by the . **Create the `slm-remote-production` GitHub Environment** with required reviewers and secrets `CONATIVE_SLM_ENDPOINT`, `CONATIVE_SLM_MODEL_NAME`, `SLM_API_KEY`. The remote smoke job is inert until then (by design). -. **Commit `src/arbiter/mix.lock`** — it cannot be generated without an - OTP toolchain; run `mix deps.get` once in `src/arbiter/` on any OTP-27 - machine and commit the lockfile. +. ~~Commit `src/arbiter/mix.lock`~~ — **done**: an OTP-27/Elixir-1.18 + toolchain was installed in the sandbox; `mix.lock` is committed, + `mix format` passes, 28/28 ExUnit tests pass locally, and the escript + protocol smokes were verified by hand. . Audit-sink hardening for production multi-instance use: decide file permissions/ownership (`umask`), add cross-process append locking if more than one arbiter writes one file, wire log shipping/backup, and consider hash-chained records for tamper evidence. The tested floor (single instance, flush-before-ack, rotation) is documented in `docs/ARBITER_PROTOCOL.adoc`. -. Watch the first `nickel-native` CI run — it is the authoritative - verification vehicle for the feature build. +. ~~Watch the first `nickel-native` CI run~~ — **done: green** on + 2026-09-22 (PR #121). The `slm-real-inference` local jobs are also + green there. +. Review the `.cargo/audit.toml` advisory exceptions (four advisories in + the optional `nickel` closure — im/imbl/bitmaps/paste — none reachable + from the default build; fixes need upstream nickel-lang-core dep moves). == Known limitations / deliberate deferrals diff --git a/src/arbiter/.formatter.exs b/src/arbiter/.formatter.exs new file mode 100644 index 0000000..e545791 --- /dev/null +++ b/src/arbiter/.formatter.exs @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) Jonathan D.A. Jewell + +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/src/arbiter/config/runtime.exs b/src/arbiter/config/runtime.exs index 04f8984..3aa1d6f 100644 --- a/src/arbiter/config/runtime.exs +++ b/src/arbiter/config/runtime.exs @@ -4,19 +4,21 @@ import Config +audit_max_bytes = + case System.get_env("CONATIVE_AUDIT_MAX_BYTES") do + nil -> + nil + + raw -> + case Integer.parse(raw) do + {value, ""} when value > 0 -> value + _ -> nil + end + end + config :conative_gating, audit_path: System.get_env("CONATIVE_AUDIT_PATH"), - audit_max_bytes: - case System.get_env("CONATIVE_AUDIT_MAX_BYTES") do - nil -> - nil - - raw -> - case Integer.parse(raw) do - {value, ""} when value > 0 -> value - _ -> nil - end - end + audit_max_bytes: audit_max_bytes # Tests must never pollute the working directory: route the default audit # path into the build tree where artifacts are disposable. diff --git a/src/arbiter/lib/conative_gating/arbiter_protocol.ex b/src/arbiter/lib/conative_gating/arbiter_protocol.ex index 0782b3a..ab01ac6 100644 --- a/src/arbiter/lib/conative_gating/arbiter_protocol.ex +++ b/src/arbiter/lib/conative_gating/arbiter_protocol.ex @@ -70,7 +70,7 @@ defmodule ConativeGating.ArbiterProtocol do "llm" => %{"confidence" => confidence}, "slm" => %{"violation_confidence" => violation_confidence}, "oracle" => %{"verdict" => oracle_verdict} - } = request + } = _request ) when is_binary(request_id) and is_number(confidence) and is_number(violation_confidence) do diff --git a/src/arbiter/lib/conative_gating/audit_log.ex b/src/arbiter/lib/conative_gating/audit_log.ex index c00b4bd..2e2a434 100644 --- a/src/arbiter/lib/conative_gating/audit_log.ex +++ b/src/arbiter/lib/conative_gating/audit_log.ex @@ -108,7 +108,10 @@ defmodule ConativeGating.AuditLog do case persist(state.path, state.max_bytes, line) do :ok -> - history = (state.history ++ [sanitized]) |> Enum.take(-state.history_capacity) + # Mirror the WIRE shape (string keys) so diagnostics see exactly what + # was persisted — no atom/string key duality for callers. + mirrored = Jason.decode!(line) + history = (state.history ++ [mirrored]) |> Enum.take(-state.history_capacity) {:reply, :ok, %{state | history: history}} {:error, reason} = error -> @@ -159,7 +162,7 @@ defmodule ConativeGating.AuditLog do end end - defp maybe_rotate(path, max_bytes, projected_size) + defp maybe_rotate(_path, max_bytes, projected_size) when projected_size <= max_bytes, do: :ok diff --git a/src/arbiter/lib/conative_gating/consensus_arbiter.ex b/src/arbiter/lib/conative_gating/consensus_arbiter.ex index 117e621..9361553 100644 --- a/src/arbiter/lib/conative_gating/consensus_arbiter.ex +++ b/src/arbiter/lib/conative_gating/consensus_arbiter.ex @@ -12,7 +12,8 @@ defmodule ConativeGating.ConsensusArbiter do use GenServer - @slm_weight 1.5 # SLM votes count 1.5x (asymmetric) + # SLM votes count 1.5x (asymmetric) + @slm_weight 1.5 def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) @@ -52,10 +53,11 @@ defmodule ConativeGating.ConsensusArbiter do no_go_score = slm.violation_confidence * @slm_weight # Add oracle soft concerns to no_go - no_go_score = case oracle.verdict do - {:soft_concern, _} -> no_go_score + 0.2 - _ -> no_go_score - end + no_go_score = + case oracle.verdict do + {:soft_concern, _} -> no_go_score + 0.2 + _ -> no_go_score + end cond do # Clear violation @@ -68,13 +70,14 @@ defmodule ConativeGating.ConsensusArbiter do # Uncertain - escalate to human true -> - {:escalate, %{ - go_score: go_score, - no_go_score: no_go_score, - llm: llm, - slm: slm, - oracle: oracle - }} + {:escalate, + %{ + go_score: go_score, + no_go_score: no_go_score, + llm: llm, + slm: slm, + oracle: oracle + }} end end end diff --git a/src/arbiter/mix.exs b/src/arbiter/mix.exs index 6420fcb..47408a0 100644 --- a/src/arbiter/mix.exs +++ b/src/arbiter/mix.exs @@ -31,8 +31,9 @@ defmodule ConativeGating.MixProject do defp deps do [ - {:rustler, "~> 0.30"}, # For Rust NIF integration - {:jason, "~> 1.4"}, + # For Rust NIF integration + {:rustler, "~> 0.30"}, + {:jason, "~> 1.4"} ] end diff --git a/src/arbiter/mix.lock b/src/arbiter/mix.lock new file mode 100644 index 0000000..3617ed7 --- /dev/null +++ b/src/arbiter/mix.lock @@ -0,0 +1,4 @@ +%{ + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, + "rustler": {:hex, :rustler, "0.38.0", "7a8906998ff0d28e3021c0a73264abcda719bda344b2e58307c6805b0f87c9b4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "704c03c1bf66be12b031c5a389347b91c81c5cb819a24b068b0de36fe4a5652a"}, +} diff --git a/src/arbiter/test/arbiter_protocol_test.exs b/src/arbiter/test/arbiter_protocol_test.exs index dff3d4f..759c5fc 100644 --- a/src/arbiter/test/arbiter_protocol_test.exs +++ b/src/arbiter/test/arbiter_protocol_test.exs @@ -48,12 +48,14 @@ defmodule ConativeGating.ArbiterProtocolTest do assert {:error, {:out_of_range, "llm.confidence"}} = ArbiterProtocol.decode_request(line) line = request_json(%{"slm" => %{"violation_confidence" => -0.1}}) + assert {:error, {:out_of_range, "slm.violation_confidence"}} = ArbiterProtocol.decode_request(line) end test "unknown oracle verdict is rejected" do line = request_json(%{"oracle" => %{"verdict" => "uncertain"}}) + assert {:error, {:unknown_oracle_verdict, "uncertain"}} = ArbiterProtocol.decode_request(line) end diff --git a/src/arbiter/test/audit_log_test.exs b/src/arbiter/test/audit_log_test.exs index 8ce2f41..20aa2aa 100644 --- a/src/arbiter/test/audit_log_test.exs +++ b/src/arbiter/test/audit_log_test.exs @@ -8,14 +8,20 @@ defmodule ConativeGating.AuditLogTest do alias ConativeGating.AuditLog defp tmpdir!(tag) do - dir = Path.join(System.tmp_dir!(), "conative-audit-test-#{tag}-#{System.unique_integer([:positive])}") + dir = + Path.join( + System.tmp_dir!(), + "conative-audit-test-#{tag}-#{System.unique_integer([:positive])}" + ) + File.mkdir_p!(dir) on_exit(fn -> File.rm_rf(dir) end) dir end defp start_sink(dir, opts) do - {:ok, pid} = AuditLog.start_link(Keyword.merge([path: Path.join(dir, "audit.jsonl"), name: nil], opts)) + {:ok, pid} = + AuditLog.start_link(Keyword.merge([path: Path.join(dir, "audit.jsonl"), name: nil], opts)) on_exit(fn -> if Process.alive?(pid), do: GenServer.stop(pid) @@ -51,10 +57,13 @@ defmodule ConativeGating.AuditLogTest do sink = start_sink(dir, []) entry = %{ + "content" => "super-secret-proposal-body-xyzzy", request_id: "req-secret", verdict: "block", - "content" => "super-secret-proposal-body-xyzzy", - votes: %{"content" => "nested-secret-xyzzy", slm: %{violation_confidence: 0.9}} + votes: %{ + "content" => "nested-secret-xyzzy", + slm: %{violation_confidence: 0.9} + } } assert :ok = AuditLog.record(entry, sink) @@ -82,14 +91,24 @@ defmodule ConativeGating.AuditLogTest do refute active =~ "req-1" assert active =~ "req-2" - # A third record also fits (rotation happens lazily per record). + # A third ~107-byte record exceeds the budget AGAIN (107+107 > 200), so + # rotation is lazy-per-record: audit.jsonl.1 now holds req-2 and the + # active file contains only req-3. No record is ever lost. assert :ok = AuditLog.record(%{request_id: "req-3", reason: filler}, sink) lines = path |> File.read!() |> String.split("\n", trim: true) - assert length(lines) == 2 + assert length(lines) == 1 + assert File.read!(path <> ".1") =~ "req-2" + assert hd(lines) =~ "req-3" + + # Union of active + rotated still carries every record written. + rotated2 = File.read!(path <> ".1") + assert rotated =~ "req-1" and rotated2 =~ "req-2" and hd(lines) =~ "req-3" end test "persistence failure fails closed" do - missing_parent = Path.join(System.tmp_dir!(), "conative-missing-#{System.unique_integer([:positive])}") + missing_parent = + Path.join(System.tmp_dir!(), "conative-missing-#{System.unique_integer([:positive])}") + bad_path = Path.join(missing_parent, "audit.jsonl") {:ok, sink} = AuditLog.start_link(path: bad_path, name: nil) diff --git a/src/arbiter/test/protocol_server_test.exs b/src/arbiter/test/protocol_server_test.exs index bdab6e6..26d16d7 100644 --- a/src/arbiter/test/protocol_server_test.exs +++ b/src/arbiter/test/protocol_server_test.exs @@ -8,7 +8,12 @@ defmodule ConativeGating.ProtocolServerTest do alias ConativeGating.{AuditLog, ProtocolServer} defp tmpdir!(tag) do - dir = Path.join(System.tmp_dir!(), "conative-server-test-#{tag}-#{System.unique_integer([:positive])}") + dir = + Path.join( + System.tmp_dir!(), + "conative-server-test-#{tag}-#{System.unique_integer([:positive])}" + ) + File.mkdir_p!(dir) on_exit(fn -> File.rm_rf(dir) end) dir @@ -44,10 +49,11 @@ defmodule ConativeGating.ProtocolServerTest do assert response["verdict"] == "allow" assert response["audit_recorded"] == true - # Exactly one audit record for the accepted request. + # Exactly one audit record for the accepted request. History mirrors the + # persisted wire shape (string keys) by design. [entry] = AuditLog.history(sink) - assert entry.request_id == "req-allow-1" - assert entry.verdict == "allow" + assert entry["request_id"] == "req-allow-1" + assert entry["verdict"] == "allow" end test "hard oracle violation blocks" do @@ -88,12 +94,16 @@ defmodule ConativeGating.ProtocolServerTest do end test "audit failure yields an error response, never an unaudited verdict" do - missing_parent = Path.join(System.tmp_dir!(), "conative-missing-#{System.unique_integer([:positive])}") + missing_parent = + Path.join(System.tmp_dir!(), "conative-missing-#{System.unique_integer([:positive])}") + {:ok, sink} = AuditLog.start_link(path: Path.join(missing_parent, "audit.jsonl"), name: nil) on_exit(fn -> if Process.alive?(sink), do: GenServer.stop(sink) end) - response = ProtocolServer.process_line(request_line("req-audit-fail"), sink) |> Jason.decode!() + response = + ProtocolServer.process_line(request_line("req-audit-fail"), sink) |> Jason.decode!() + assert response["request_id"] == "req-audit-fail" assert response["error"] =~ "audit persistence failed" refute Map.has_key?(response, "verdict") diff --git a/src/contract/src/arbiter.rs b/src/contract/src/arbiter.rs index 8956dae..4b920e4 100644 --- a/src/contract/src/arbiter.rs +++ b/src/contract/src/arbiter.rs @@ -223,12 +223,28 @@ impl ArbiterClient { })?; // Write the request and close stdin so stream-driven servers exit. - child + // + // A BrokenPipe here is NOT fatal: a fast, broken, or non-protocol + // arbiter may exit before the write lands, closing the read end of + // our stdin pipe. That is exactly the failure mode the read/validate + // path below already classifies correctly (no answer → `Closed`, + // garbage answer → `Malformed`), so swallow EPIPE and let the + // exit/read path decide. Any other write error is a real transport + // failure. + let write_result = child .stdin .take() .expect("invariant: stdin piped") - .write_all(line.as_bytes()) - .map_err(|error| ArbiterError::Transport(format!("write to arbiter stdin: {error}")))?; + .write_all(line.as_bytes()); + if let Err(error) = write_result { + if error.kind() != std::io::ErrorKind::BrokenPipe { + let _ = child.kill(); + let _ = child.wait(); + return Err(ArbiterError::Transport(format!( + "write to arbiter stdin: {error}" + ))); + } + } let mut stdout = child.stdout.take().expect("invariant: stdout piped"); let reader = std::thread::spawn(move || { @@ -442,10 +458,11 @@ mod tests { let dir = fixture_dir(); let script = make_script(&dir, "arbiter-garbage.sh", "echo 'not json at all'\n"); let client = client_for(&script); - assert!(matches!( - client.decide(0.5, 0.5, OracleVote::Allow), - Err(ArbiterError::Malformed(_)) - )); + let result = client.decide(0.5, 0.5, OracleVote::Allow); + assert!( + matches!(result, Err(ArbiterError::Malformed(_))), + "garbage output must classify as Malformed, got {result:?}" + ); std::fs::remove_dir_all(&dir).ok(); } @@ -455,10 +472,11 @@ mod tests { let dir = fixture_dir(); let script = make_script(&dir, "arbiter-closed.sh", "exit 0\n"); let client = client_for(&script); - assert!(matches!( - client.decide(0.5, 0.5, OracleVote::Allow), - Err(ArbiterError::Closed(_)) - )); + let result = client.decide(0.5, 0.5, OracleVote::Allow); + assert!( + matches!(result, Err(ArbiterError::Closed(_))), + "immediate exit must classify as Closed, got {result:?}" + ); std::fs::remove_dir_all(&dir).ok(); } diff --git a/vendor/bunsenite/VENDOR.adoc b/vendor/bunsenite/VENDOR.adoc index 21e597c..df87416 100644 --- a/vendor/bunsenite/VENDOR.adoc +++ b/vendor/bunsenite/VENDOR.adoc @@ -66,3 +66,11 @@ paths deliberately. conative-gating therefore takes the explicit position documented in `docs/NICKEL-POLICY.adoc`: **policy files with `import` statements are rejected fail-closed before evaluation**. This vendor fork inherits that policy by usage, not by code change. + +== Estate CI interactions + +The dogfood K9-contract validator scans every `.k9.ncl` in the repo; the +upstream contractiles under `vendor/bunsenite/**` are curated (and +validated) upstream, so the repo workflow passes +`INPUT_PATHS_IGNORE: "/vendor/bunsenite/"` to `validate-k9.sh` +(see `.github/workflows/dogfood-gate.yml`). From afa3fb3df70ab3b712ff99dc8d1c5ac9518e8107 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:56:06 +0000 Subject: [PATCH 2/3] docs(arbiter): clarify decision validation and process error handling --- src/contract/src/arbiter.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/contract/src/arbiter.rs b/src/contract/src/arbiter.rs index 4b920e4..702aa77 100644 --- a/src/contract/src/arbiter.rs +++ b/src/contract/src/arbiter.rs @@ -186,7 +186,15 @@ impl ArbiterClient { Ok(Some(Self::new(&parts, DEFAULT_ARBITER_TIMEOUT)?)) } - /// Ask the arbiter for a consensus decision. + /// Ask a short-lived arbiter process for one consensus decision. + /// + /// A decision is returned only when the response uses protocol v1, matches + /// the generated request ID, and confirms that its audit record persisted. + /// If the arbiter closes its input before the request is written, response + /// handling still classifies an empty reply as [`ArbiterError::Closed`] or + /// malformed output as [`ArbiterError::Malformed`]. Other process I/O + /// failures return [`ArbiterError::Transport`], and an unresponsive arbiter + /// returns [`ArbiterError::Timeout`]. pub fn decide( &self, llm_confidence: f64, From fa290e9463a1d0ce561a84efe97605a89861dda2 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:14:16 +0100 Subject: [PATCH 3/3] Document arbiter decision errors when request writes fail (#124) Clarify the `decide` docstring: when the arbiter closes input before the request is fully written, the response determines the outcome; missing responses return `Closed`, malformed output returns `Malformed`, and other write failures return `Transport`. Documentation only; no runtime behavior changes. Validation was not run. [View coding task](https://app.coderabbit.ai/code/tasks/d8bce459-a235-5b9b-9671-136ca976a30e?source=coding_agent_github_pr_description) Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>