Skip to content

fix(mesh): add inference-only embedded peer surface - #1

Open
ccarpene-blk wants to merge 516 commits into
block:mainfrom
ccarpene-blk:ccarpene-blk/fix/meshllm-inference-only
Open

fix(mesh): add inference-only embedded peer surface#1
ccarpene-blk wants to merge 516 commits into
block:mainfrom
ccarpene-blk:ccarpene-blk/fix/meshllm-inference-only

Conversation

@ccarpene-blk

@ccarpene-blk ccarpene-blk commented Jul 30, 2026

Copy link
Copy Markdown

Summary

  • reject non-inference requests on inbound peer HTTP tunnels before connecting to the local API proxy
  • add an opt-in peer_inference_only embedded mode that keeps mesh routing and OpenAI inference streams while rejecting raw tunnels, plugins, subprotocols, and Skippy stage ALPN from peers
  • stop restricted embedded nodes from auto-importing globally installed plugins from the host plugin store
  • preserve the default full MeshLLM peer surface and installed-plugin discovery for existing callers unless they opt into the stricter embedded mode
  • add live iroh regressions proving allowed inference still reaches the local proxy while HTTP control routes and stage-control requests are rejected

Context

Buzz currently pins MeshLLM at f455d493a2ae82baf2a326e2d0fda351433b4b30, where the inbound QUIC HTTP tunnel forwards arbitrary HTTP requests to the local :9337 proxy. That means a trusted mesh peer can reach control routes such as /mesh/load and /mesh/drop, not just inference. A deeper review also found that admitted peers can reach non-HTTP mesh capabilities such as Skippy stage control and plugin/subprotocol streams.

This patch makes the intended contract explicit for embedded consumers: peer_inference_only(true) limits remote peers to mesh maintenance/routing plus the OpenAI inference tunnel, and the tunnel itself allows only model discovery and inference routes. Restricted embedded runtimes also stop appending ambient plugins discovered from the process-wide installed plugin store. Local loopback APIs and the default MeshLLM CLI behavior remain unchanged.

block/mesh-llm is currently behind the upstream revision Buzz consumes, so this PR includes the upstream catch-up needed to reach the affected tunnel implementation plus the narrow fix on top.

Verification

  • cargo fmt --all --check
  • RUSTFLAGS="-C link-arg=-fuse-ld=ld" cargo test -p mesh-llm-host-runtime --no-default-features --features dynamic-native-runtime restricted_host_mode_does_not_import_ambient_installed_plugins --lib
    • restricted plugin resolution ignores an ambient installed plugin while normal resolution still includes it
  • RUSTFLAGS="-C link-arg=-fuse-ld=ld" cargo test -p mesh-llm-host-runtime --no-default-features --features dynamic-native-runtime network::tunnel --lib
    • 8 tunnel tests passed, including the live restricted-mode iroh peer-tunnel boundary test
  • RUSTFLAGS="-C link-arg=-fuse-ld=ld" cargo test -p mesh-llm-host-runtime --no-default-features --features dynamic-native-runtime inference_only_peer_surface_keeps_routing_but_blocks_extended_capabilities --lib
  • RUSTFLAGS="-C link-arg=-fuse-ld=ld" cargo test -p mesh-llm-host-runtime --no-default-features --features dynamic-native-runtime inference_only_peer_surface_rejects_stage_control_after_admission --lib
    • admitted peer stage-control is rejected in restricted mode
  • RUSTFLAGS="-C link-arg=-fuse-ld=ld" cargo check -p mesh-llm-host-runtime --no-default-features --features dynamic-native-runtime
  • RUSTFLAGS="-C link-arg=-fuse-ld=ld" cargo clippy -p mesh-llm-host-runtime --no-default-features --features dynamic-native-runtime --all-targets -- -D warnings
  • RUSTFLAGS="-C link-arg=-fuse-ld=ld" MESH_LLM_SKIP_UI=1 cargo build -p mesh-llm --bin mesh-llm
  • MESH_TWO_NODE_MAX_WAIT=240 scripts/ci-two-node-client-serving-smoke.sh target/debug/mesh-llm target/debug /tmp/SmolLM2-135M-Instruct-Q8_0.gguf
    • host readiness passed
    • client-routed /v1/models passed
    • client-routed non-streaming chat passed
    • client-routed streaming chat passed

Notes

  • The full mesh-llm-host-runtime lib test suite was not used as a signal locally because many unrelated tests require socket capabilities that are blocked in the default sandbox; the focused socket tests and real two-node smoke were run outside the sandbox instead.
  • The current zizmor and Semgrep OSS check failures are annotations on imported upstream .github/workflows/* and composite action files from the Block fork catch-up, not on the runtime files changed by this boundary patch. The Block mirror needs a sync/base-strategy decision before this can merge cleanly.
  • Buzz still needs a follow-up dependency bump and .peer_inference_only(true) opt-in after this commit is available from the Block fork; it should not pin to a personal fork or PR ref.

i386 and others added 30 commits May 26, 2026 06:07
* ci: make PR rust cache restores non-fatal
* ci: clean up workflow lint warnings
* ci: gate Windows Node release addon builds
* ci: remove Docker builds from PRs
* Update PR CI routing and cache warm keys
* Document PR CI timing heuristics
* Stop CI-only changes from fanning out
* Document CI-only routing contract
* Trigger & test React build timing
* updates to ROADMAP.md

* Update ROADMAP.md

* Update ROADMAP.md
Fix Skippy prefix-cache reuse for shared-prefix agent prompts

Skippy split-prefill recording now stores the same bounded shared-prefix candidates that lookup already probes. This lets repeated agent/tool prompts with a stable long prefix and changing tail reuse the resident prefix cache instead of falling back to only exact-prompt hits.

The change keeps exact prompt records intact, preserves the existing bounded candidate policy, and adds regressions proving that same-prefix/different-tail prompts share the recorded grid page while distinct exact prompts remain distinct.

Validation
* Validation tier: Tier 2 - narrow Skippy prefix-cache record/lookup repair for shared-prefix agent prompts.
* git fetch --no-tags origin main:refs/remotes/origin/main: PASS, origin/main at 9b51d01.
* git rebase origin/main: PASS.
* git diff --check origin/main...HEAD: PASS, no output
* git diff --check: PASS, no output
* git diff --cached --check: PASS, no output
* cargo fmt --all -- --check: PASS
* cargo test -p skippy-cache --lib: PASS, 31 passed
* LLAMA_STAGE_BUILD_DIR=<prepared llama stage build> cargo test -p skippy-server prefix --lib: PASS, 4 passed
* LLAMA_STAGE_BUILD_DIR=<prepared llama stage build> cargo test -p skippy-server --lib: PASS, 98 passed
* LLAMA_STAGE_BUILD_DIR=<prepared llama stage build> cargo check -p mesh-llm: PASS
* LLAMA_STAGE_BUILD_DIR=<prepared llama stage build> cargo-clippy clippy -p skippy-server --all-targets -- -D warnings: PASS
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - cache behavior repair only; no release/version sync required.
* Not run: live shared-prefix agent smoke - no local model/runtime endpoint was available; targeted identity/server tests cover the record/lookup path.

Rollback
* git revert HEAD
Fix Blacksmith CI discrepancies
Fix metadata-only package verification cache
Fix dispatched Swift release manifest flow
Validation
* Validation tier: Tier 4 - release publish verification tooling and crates.io partial-publish recovery for issue Mesh-LLM#691.
* git fetch --no-tags origin main:refs/remotes/origin/main: PASS, origin/main at 5c805ea.
* git diff --check: PASS, no output
* git diff --cached --check: PASS, no output
* bash -n scripts/publish-crates.sh: PASS
* scripts/publish-crates.sh --allow-dirty: PASS, rejected because --allow-dirty requires --dry-run
* python3 -m unittest scripts.tests.test_publish_crates: PASS, 7 passed
* python3 -m unittest discover -s scripts/tests: PASS, 27 passed
* cargo run -p xtask -- repo-consistency release-targets: PASS
* CARGO_TARGET_DIR=$(mktemp -d /tmp/mesh-llm-publish-target.XXXXXX) scripts/publish-crates.sh --dry-run --allow-dirty: PASS; dry-run verified publishable crates through model-artifact and deferred downstream crates whose 0.66.0 registry dependencies are not published yet.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - release publish tooling/docs only; no release version sync required.
* Not run: actionlint - not installed locally and workflow file was not changed.
* Not run: real cargo publish / just release - intentionally avoided for PR validation.

Rollback
* git revert HEAD
Summary
Adds a repeatable KV/tool-loop stability certification harness for direct-model agent/tool-call pressure runs, including plan output, manifest evidence, transcript handling, native-log checkpoint scanning, docs, tests, and a repo-local agent skill.

Validation
* Validation tier: Tier 4 - verification tooling and testing documentation for KV/tool-loop stability certification; no runtime, protocol, workflow, or CI gate change.
* git fetch --no-tags origin main:refs/remotes/origin/main: PASS, origin/main at 12aa611.
* git rebase origin/main: PASS, rebased cleanly after Mesh-LLM#695/Mesh-LLM#696.
* git diff --check origin/main...HEAD: PASS, no output
* git diff --check: PASS, no output
* git diff --cached --check: PASS, no output
* python3 -m unittest scripts.tests.test_qa_kv_tool_loop_stability: PASS, 16 passed
* python3 -m unittest discover -s scripts/tests: PASS, 43 passed
* python3 -m py_compile scripts/qa-kv-tool-loop-stability.py scripts/tests/test_qa_kv_tool_loop_stability.py: PASS
* mkdir -p target/kv-tool-loop-stability && python3 scripts/qa-kv-tool-loop-stability.py --models auto,mesh --attempts 1 --pressure-turns 2 --timeout 30 --min-cached-tokens 128 --suffix-prefill-limit 64 --output-dir target/kv-tool-loop-stability/review-smoke --print-plan > /tmp/kv-tool-loop-plan.json && python3 -m json.tool /tmp/kv-tool-loop-plan.json >/dev/null: PASS
* Remote PR checks after rebase: PASS, changes + summary green; non-applicable build/test matrices skipped by path filters.
* Review/merge state: APPROVED, MERGEABLE, CLEAN.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - verification tooling/docs only; no release/version sync required.
* Not run: cargo check/tests - not required for selected validation tier; no Rust/runtime code changed.
* Not run: live direct-model Skippy certification run - no local loaded direct-model endpoint was available; deterministic unit and plan-smoke coverage proves the harness behavior.

Rollback
* Revert this PR.
…#697)

* skippy-runtime: filter DEBUG-level lines from native log callback

llama.cpp's llama_log_internal_v invokes the registered log callback
unconditionally with the level argument — the level filter is expected
to live inside the callback. mesh-llm's native log callback
(write_native_log) was ignoring _level and writing every line to
~/.mesh-llm/runtime/<pid>/logs/skippy-native.log, which meant
LLAMA_LOG_DEBUG output ended up in the file regardless of
GGML_LLAMA_LOG_LEVEL.

Observed impact: on studio today, ~5400 'Grammar still awaiting
trigger after token …' DEBUG lines for 18 actual tool-call grammar
triggers — roughly 300× verbosity per completion. The lines are emitted
per-token during MiniMax-M2.5's reasoning phase before the
<minimax:tool_call> regex fires.

Fix: drop ggml_log_level=DEBUG (1) unless GGML_LLAMA_LOG_LEVEL=4 (the
existing opt-in that enable_verbose_native_logs sets). Keep CONT
(continuation) lines regardless so multi-line INFO/WARN messages don't
get truncated.

Tests added in skippy-runtime::tests:
- native_log_filter_drops_debug_by_default
- native_log_filter_keeps_debug_when_verbose
- native_log_filter_keeps_continuation_lines

Env-var tests run serially via per-test mutex to avoid racing the rest
of the file's GGML_LLAMA_LOG_LEVEL-touching tests.

* mesh: harden relay-only heartbeat threshold against path flap

Three related changes to the heartbeat failure-detection path:

1) Bump relay-only failure threshold from 3 to 5.

   Observed today on the public mesh: a Sydney<->Sydney peer (mini ->
   studio) that should hole-punch a direct LAN path but doesn't (mini's
   macOS VPN system extension blocks the UDP hole-punch) is forced onto
   a relay-only path via services.iroh.computer. Steady-state relay RTT
   is ~200ms but transient renegotiation spikes it to 10s+. With 60s
   heartbeats and a 3-miss threshold, mini declared studio dead twice
   in a 25 minute window — and each declaration knocked MiniMax-M2.5
   out of the MoA reducer's eligible-peer set, causing
   'HTTP 502: Reducer failed (tried 3)' for ~90s while the fallback to
   weaker peers also failed. 5 misses = 5 min grace covers the typical
   iroh relay path-renegotiation window. Direct paths stay at 2.

2) Fix is_relay_only detection so it survives mid-failure.

   The original check was ,
   which returns  (not Relay) when no path is currently
   selected — exactly what happens during a heartbeat failure when the
   connection is between path selections. That meant the
   relay-only-grace policy *never fired during the failures it was
   designed to protect against*: every flap landed on the strict
   direct-threshold path.

   New is_relay_only_connection inspects every advertised path; if none
   is IP (i.e. only relay paths are known), treat as relay-only. Also
   fixes the no-connection case: previously a peer with no live
   Connection object got the strict direct threshold, which is
   backwards — no connection at all is the most failure-prone state.
   Now defaults to lenient.

3) Cosmetic: '💚 Heartbeat: <peer> recovered (was N/2)' was hardcoded
   '/2', misleading for relay-only peers whose actual threshold was 3
   (now 5). Read the real threshold from the policy and display it.

Tests (mesh::tests::):
- relay_only_peers_get_extra_heartbeat_grace (renamed,
  threshold updated 3 -> 5)
- direct_peers_use_strict_heartbeat_threshold (new)
- is_relay_only_path_set_classifies_correctly (new,
  exercises empty/all-relay/mixed/all-direct path sets)

is_relay_only_connection is implemented as a thin wrapper over
is_relay_only_path_set(IntoIterator<Item=bool>) so the classification
logic is unit-testable without constructing a real iroh Connection.

* clippy: drop redundant closure in heartbeat relay-only check

* skippy: make CONT pass-through explicit in native log filter

The original ordering made the CONT check dead code: the level-!=-DEBUG
guard above it already returned false for CONT (level 5), so the
explicit CONT branch could never execute. The comment claimed CONT was
being protected by that branch but the protection was incidental.

Reorder so CONT is checked first and is the real explicit pass-through,
matching the documented intent.

* heartbeat: default no-Connection to strict, not lenient

PR review (P2, James): when state.connections.get(peer_id) returns None
the old code in this PR defaulted is_relay_only to true, which extended
the 5-min relay grace to *any* peer without a live Connection object —
including previously-direct peers that simply got cleanly disconnected
(QUIC idle-expired, peer departed). That kept stale model routes alive
for up to 5 min instead of 2 for direct paths.

The lenient threshold exists to absorb mid-flap path renegotiation,
which only happens while iroh still owns the Connection. Once
Connection is gone, the peer should be judged by the strict (direct)
threshold so the eligible-peer set converges promptly.

Introduce classify_relay_only_for_policy(Option<bool>) so the
no-Connection default is testable in isolation. Live Connection still
gets is_relay_only_connection's path-set verdict (unchanged); only the
no-Connection fallback changes from lenient → strict.

Test: classify_relay_only_defaults_to_strict_when_no_connection covers
all three Option states.

* Revert skippy-runtime DEBUG log filter — defer to a focused PR

Pulled at James's P3 feedback. The native log filter as shipped only
honors GGML_LLAMA_LOG_LEVEL=4 for DEBUG and ignores 0/1/2/3, so
values like silent / error / warn are silently ineffective. Fixing it
properly means parsing the env as a full threshold, deciding whether
to cache (Copilot's perf concern) and how CONT should behave when its
base line was filtered (James's part-b). That is a meaningful design
discussion in its own right and doesn't belong tacked onto the
heartbeat PR.

Reverts to byte-for-byte parity with main on crates/skippy-runtime/src/lib.rs
so Mesh-LLM#697 is unambiguously 'relay-only heartbeat grace + path-set
classification fix', nothing else. The log filter will land separately
once the threshold + CONT semantics are settled.

heartbeat fixes (relay-only grace bump + is_relay_only_connection
path-set classifier + classify_relay_only_for_policy strict-on-no-conn
default + recovery-message threshold) are unchanged.
* MoA: paint winner content as multiple SSE deltas

Today MoA emits the full winning text in a single SSE delta, so chat UIs
jump from spinner to wall-of-text once the arbiter commits (~3s). This
splits the buffered winner into ~25 word-boundary chunks with a short
inter-chunk delay so the response paints progressively over ~500ms.

Tool-call deltas remain atomic — harness parsers (Goose, OpenCode) still
see a single well-formed tool_call object.

Tunable via MESH_MOA_STREAM_CHUNKS (default 25) and
MESH_MOA_STREAM_CHUNK_DELAY_MS (default 20).

Verified on M4 against a real mesh:
  main:          1 delta, 0ms paint spread, TTFB 3212ms
  this branch:   22 deltas, 457ms paint spread, TTFB 3184ms

Refs Mesh-LLM#618.

* MoA: drip progress into reasoning_content while arbiter waits

Streaming MoA requests previously sat silent for ~3s until the arbiter
committed. Now the gateway sends response headers immediately and emits
short progress lines ("Routing through mesh…", "Querying peer
models…", "Comparing responses…") into delta.reasoning_content once
per second.

Goose and other OpenAI-shape clients route reasoning_content to the
thinking pane, so the lines appear as visible activity without
polluting the final answer. Clients that ignore the field (pi, openai
SDK without reasoning support) parse the chunks normally and only see
the final content.

Responses-API requests drip into output_text.delta instead, since the
Responses spec has no dedicated reasoning channel — the lines render
inline but are short enough that the real answer effectively replaces
them within a few hundred ms.

Trade-off: HTTP headers must precede the body, so this path loses the
post-hoc x-moa-* observability headers (workers, elapsed, etc.).

Verified locally on M4 against a real mesh:
  - Wire: 3 reasoning_content drips over the 3s wait, then 22-chunk
    content paint (from the previous commit on this branch)
  - Goose chat-completions: tool calls still atomic, content arrives
  - Pi chat-completions: streams correctly, ignores reasoning_content

Refs Mesh-LLM#618.

* MoA progress: drip into reasoning_text.delta on Responses path

The mesh-llm web UI (and any Responses-API consumer that follows the
OpenAI spec) routes response.reasoning_text.delta to a separate
thinking pane, but appends response.output_text.delta to the visible
answer.

Emitting progress lines as output_text.delta polluted the final
answer — the 'Routing through mesh…' / 'Querying peer models…' /
'Comparing responses…' lines stuck to the top of the assistant's
reply instead of staying in the thinking section.

Switch to response.reasoning_text.delta for the Responses adapter so
progress shows in the thinking pane and collapses cleanly when the
real answer arrives. Chat-completions path (goose etc.) continues to
use delta.reasoning_content, which is the analogous channel there.

Verified on M4 against a real mesh via /v1/responses streaming probe:
  - 3× response.reasoning_text.delta at 1s/2s/3s (progress)
  - 22× response.output_text.delta from 3.0s-3.5s (real answer)
And confirmed in the mesh-llm web console: progress shows in
thinking pane, real answer in the bubble, no concatenation.

Refs Mesh-LLM#618.

* fmt: nest matches!() under streaming guard

* MoA progress: cycle tail lines, decompose for clippy

Two small follow-ups on the streaming MoA work after live-testing
against a slow public peer:

1. Progress lines now cycle through a tail set after the initial
   three. Previously a slow MoA call (10-20s, common on public mesh
   when a peer is loaded) repeated 'Still working…' over and over.
   Now the opening sequence (Routing / Querying / Comparing) fires
   once each, then the tail (Waiting on a slow peer / Still
   gathering / Hold on…) cycles deterministically.

2. Split run_moa_turn_with_progress into three named helpers
   (send_progress_headers, drip_progress_until_moa_completes,
   write_progress_body) so each piece stays under the clippy
   cognitive-complexity threshold. Extracted the duplicated
   SSE-header-write block in send_moa_as_*sse_inner to a shared
   write_sse_response_headers helper.

Tests updated to cover the cycle behaviour and renamed to match.

* MoA streaming: drop env-var overrides, constants only

Copilot review on PR Mesh-LLM#685 flagged that the MESH_MOA_STREAM_CHUNKS
and MESH_MOA_STREAM_CHUNK_DELAY_MS env vars contradicted the PR
description ("no env vars") and that the env-mutating tests were
racy under parallel test runs.

- Replace DEFAULT_MOA_STREAM_CHUNKS + getter with const MOA_STREAM_CHUNKS = 25
- Replace DEFAULT_MOA_STREAM_CHUNK_DELAY_MS + getter with const MOA_STREAM_CHUNK_DELAY = 20ms
- Rename MOA_STREAM_MIN_CHARS → MOA_STREAM_MIN_BYTES (the check is .len(), not chars().count(); fixes a doc-vs-code mismatch Copilot flagged separately)
- Delete env-var mutation in chat_sse / responses_sse delta tests; accept the ~500ms test runtime each (real-world chunk delay × N)
- Rename chunk_helper_empty_input_returns_empty_slice →
  chunk_helper_empty_input_returns_single_empty_chunk (the helper
  returns vec![""], not an empty vec; better failure message)

No runtime behaviour change — the defaults were already the values
the env vars defaulted to, and no production code path relied on
the overrides.

* MoA progress: id-consistency + Responses ordering fixes

Two correctness bugs flagged by Copilot review on PR Mesh-LLM#685:

1. Progress chunks hard-coded id=chatcmpl-mesh, but the body writer
   used the real MoA id (chatcmpl-moa-<hex>). Clients that
   correlate stream chunks by id (most chunk-aggregating SDKs) saw
   the progress and the content as belonging to different
   completions.

2. Responses-API streams emitted reasoning_text.delta events BEFORE
   response.created (the progress drip fired during MoA wait;
   response.created came from the body writer once MoA finished).
   The Responses-API contract is strict: created must precede all
   delta events, otherwise strict clients reject the stream. The
   body writer was also emitting a second response.created (now
   two in one stream — also a protocol violation).

Changes:

- Generate a single chatcmpl-moa-<hex-nanos> id up front in
  run_moa_turn_with_progress (same shape as MoA's own short_id).
- Thread it through write_progress_event, write_failure_as_sse_tail.
- After MoA returns, mutate response_body.id so the body writer
  reuses the same id (no new helper to plumb the id into the
  body writer).
- On the Responses path, emit response.created up front right after
  headers via a new write_progress_response_created helper.
- send_moa_as_responses_sse_inner: when header_already_sent is true,
  skip its own response.created (the progress path already sent it).
- item_id derived from completion_id via item_id_from_completion_id
  helper, mirroring short_id_from_response shape so progress and
  final events share item_id within one Responses stream.
- Failure-tail also reuses the completion_id and adds 'id' on the
  Responses error envelope.

Tests added:
- progress_event_chat_uses_reasoning_content_field: now asserts id
  matches the supplied completion_id.
- progress_event_responses_uses_reasoning_text_delta: now asserts
  item_id derives correctly from completion_id.
- failure_tail_emits_error_then_done_for_chat_adapter: now asserts
  the failure chunk carries the completion_id.
- responses_progress_path_emits_exactly_one_response_created: new,
  end-to-end test that runs headers → created → progress delta →
  body writer (header_already_sent=true) and asserts exactly one
  response.created event, in the right ordering position.
- item_id_derives_short_suffix_from_completion_id: new unit test
  for the helper.

Verified live on M4 against the public mesh:
- Wire probe: 1 response.created, 3 reasoning_text.delta, 30
  output_text.delta. All 34 events share id chatcmpl-moa-<X> and
  item_id msg_moa_<X>.
- Goose tool-call (write a file): tool argument parsed correctly,
  file produced, content matches.

* fmt: collapse send_responses_created_for_progress signature

* clippy: collapse nested inter-chunk delay ifs in MoA streaming

* moa progress: sequence_number on Responses deltas + Skip ticker behavior

Two correctness fixes for the streaming progress path.

1. Responses-API progress events (response.reasoning_text.delta) now
   carry sequence_number, starting at 1 to follow response.created at 0.
   Strict Responses clients (OpenAI SDK, Vercel AI SDK) use the field
   for ordering and dedup within a single response stream; omitting it
   was inconsistent with the helpers in openai_frontend::responses and
   with the body writer's events.

2. The 1s progress ticker now uses MissedTickBehavior::Skip. The default
   (Burst) would fire the ticker N times back-to-back if a progress
   write stalled (slow client, transient backpressure), dumping a burst
   of progress lines once writes resumed. Skip drops the missed ticks
   so cadence stays at one line per real interval.

Tests:
- progress_event_responses_uses_reasoning_text_delta now asserts
  sequence_number == 1.
- New progress_event_responses_sequence_number_increments verifies
  two consecutive writes produce monotonically increasing
  sequence_number (1, 2).

* moa progress: stream-wide monotonic sequence + cancel on client disconnect

Two fixes for the Responses-API progress streaming path (PR review,
P1 and P2 from James).

P1 — stream-wide monotonic sequence_number + stable created_at:

Previously the body writer (send_moa_as_responses_sse_inner) called
helpers that hard-code sequence_number=0 and recomputed created_at
locally, so a slow MoA response produced a wire stream like
  created(seq=0, created_at=T1)
  reasoning.delta(seq=1), reasoning.delta(seq=2), …
  output_text.delta(seq=0)   ← reset, collides with created
  output_text.done(seq=0)
  completed(seq=0, created_at=T2)   ← different timestamp, same id
Strict Responses-API clients (OpenAI SDK, Vercel AI SDK) use
sequence_number for ordering/dedup and created_at to correlate the
response object across events; both invariants were broken.

Thread the running sequence_number and the original created_at from
the progress phase into the body writer via a new
ProgressContinuation struct, and switch to the *_with_sequence helper
variants. Non-progress callers pass None and the existing behaviour
(created_at = now, sequence_number starts at 0) is preserved.

P2 — cancel MoA on client disconnect:

When a progress write fails (almost always: client closed the TCP
connection), the old code logged 'falling back' and then *awaited*
the MoA future to completion, burning peer compute and reducer
budget for a dead request — up to MoA's ~60s timeout.

drip_progress_until_moa_completes now returns
Result<(TurnResult, i32), ClientGone>. On Err the caller bails out
and the pinned MoA future is dropped, cancelling worker dispatch and
any reducer call at the next .await point.

Refactor split out drip_progress_against_future as the generic core
(takes any Future<Output = TurnResult>) so tests can substitute a
hand-rolled never-yielding future to verify the drop-on-disconnect
behaviour without running a real MoA gateway. run_moa_turn_with_progress
also gained two small helpers (send_progress_response_created_if_responses,
drip_progress_phase) to keep clippy's cognitive_complexity check
satisfied.

New tests:
- responses_progress_path_emits_monotonic_sequence_and_stable_created_at
  parses the full wire trace and asserts sequence_number is strictly
  monotonic with no duplicates AND created_at matches between
  response.created and response.completed.
- progress_drops_moa_future_when_client_disconnects uses a
  DropTrackingPendingFuture + tokio::test(start_paused) to verify
  the MoA future is dropped (not awaited) when the client socket
  closes mid-progress.

* moa_gateway: extract progress streaming into its own module

PR review (P3, James): moa_gateway.rs grew to 3,020 lines as the
streaming progress UX accreted around the gateway entry, well over
the repo's 2,000-line file limit. Split the progress-streaming
surface into a dedicated progress submodule so the gateway file can
stay focused on routing, scoring, and worker dispatch.

The progress module owns:
- The streaming entry (run_moa_turn_with_progress) and the ticker
  drip loop (drip_progress_phase, drip_progress_until_moa_completes,
  drip_progress_against_future).
- All progress-specific wire writers (write_progress_response_created,
  send_progress_headers, write_progress_event, write_progress_body,
  write_failure_as_sse_tail).
- The Responses-API continuation type (ProgressContinuation,
  ClientGone) and the small helpers tied to the progress path
  (item_id_from_completion_id, overwrite_response_id,
  progress_line, short_hex_nanos).
- All progress-related tests.

The gateway module keeps the body writers (send_moa_as_*_sse_inner)
and shared helpers (is_moa_failure_body, write_sse_response_headers,
chunking, MoA decision logic). Helpers needed by progress are
re-scoped to pub(in crate::network::openai::moa_gateway) so the
visibility boundary matches the module tree.

mod.rs: 1,953 lines. progress.rs: 1,108 lines. Both now under
the 2,000-line limit. No behaviour change \u2014 all 53 existing tests
pass without modification (just relocated into the progress test
module where they live next to the code they exercise).
* Add optional SDK console asset server

* extract shared mesh config crate

* Include console server crate in client Docker build

* Address config crate review feedback
* feat(cli): --relay-auth URL=TOKEN for gated iroh-relays

Adds a per-relay bearer token to the iroh relay map so mesh-llm can
register with a gated iroh-relay (one running AccessConfig::Restricted)
while public relays in the same map continue to register without auth.

The token is sent as 'Authorization: Bearer <TOKEN>' on the WebSocket
upgrade to the matching --relay URL, via iroh::RelayConfig::with_auth_token.

Repeatable. Splits on the first '=' only so tokens may contain '='
(base64 padding, JWTs, etc.).

Why: enables embedders (e.g. Sprout) to launch mesh-llm against an
operator-hosted iroh-relay that authenticates members via a bearer
scheme (NIP-98, JWT, opaque API key, ...) without forking mesh-llm.
Admission policy stays at the relay; mesh-llm just carries the token.

Threaded through Node::start -> bind_mesh_endpoint and
maybe_start_control_listener -> configure_control_relay so both the
data-plane and owner-control endpoints honour per-relay tokens.

Tests: parser handles trailing '=' and rejects malformed input;
relay-map builder leaves untokened relays unauthenticated and attaches
tokens only to matching URLs.

* fix(clippy): group relay urls/auths into RelayConfig to keep Node::start under arg limit

* test(relay-auth): in-process gated-relay e2e + scanner regression for --relay-auth before serve/client

PR feedback fixes for Mesh-LLM#641:

- ndizazzo: add --relay-auth to normalize_runtime_surface_args' value-taking
  flag list so 'mesh-llm --relay-auth URL=TOKEN serve/client …' no longer
  stops scanning at the token. Adds two regression tests covering both
  surfaces and a base64-padded NIP-98-style token.

- Copilot: drop the broken intra-doc link [`RelayMap`] (RelayMap isn't in
  scope at the call site) for [`iroh::RelayMap`].

Plus a real defence for the feature itself: spin up an in-process
iroh-relay with AccessConfig::Restricted, build an iroh::Endpoint from
relay_map_from_urls' output, and assert:

  1. Matching token → endpoint.online() resolves.
  2. Wrong token → home_relay_status reports 'not authorized' and
     online() never resolves.
  3. Missing token → online() never resolves.
  4. Mixed map (gated + public) authenticates only the gated relay and
     still comes online.

This is the missing end-to-end check: if iroh changes how
with_auth_token is sent on the WebSocket upgrade, or if a future refactor
drops relay_auths from the call chain, these tests fail.

* fix(relay-auth): redact token portion from parser error messages

Copilot review surfaced a real leak: parse_relay_auth_pair includes the
full URL=TOKEN input in error strings via {s:?}. If a user mistypes the
flag, the bearer token ends up in terminal output, logs, and bug
reports.

Redaction rules:

- Missing '=' separator: redact whole input (we cannot tell URL from
  token).
- Empty URL ('=token'): redact (the value after '=' is the secret).
- Empty token ('URL='): URL is safe to name; no token to leak.

New test parser_errors_never_leak_token_portion pins the property:
inject a known token string, drive each error path, assert the string
never appears in the error message.

* test(mesh): bind empty relay-auth map locally instead of &HashMap::new()

Copilot review pass flagged 7 sites where the test threads
`&std::collections::HashMap::new()` straight into
`maybe_start_control_listener(...).await`. Compiles fine today because
the callee only borrows the reference synchronously before any internal
`.await`, but the pattern is fragile: if the signature ever shifts the
borrow across an await point, every test breaks at once.

Centralise on an `empty_relay_auths()` helper bound to a local before
each call. Cheap future-proofing, removes 7 lookalikes from review
chatter on future PRs touching this file, no behaviour change.

All 119 mesh tests still pass.

* style: rustfmt for Rust 2024 edition import ordering

cargo fmt under edition 2024 sorts uppercase types alongside lowercase
modules, which reorders the gated_relay_e2e_tests imports. Pure
formatting, no logic change.

Also adds dist/native-sdk*/ and dist/llama-stage-static/ to .gitignore
so locally-packaged release artifacts don't leak into commits.
* Fix split serving for HF layer packages

* Trim split docs quickstart

* Add two-node split smoke CI

* Watch both nodes in split smoke

* Harden layer package stage preparation (Mesh-LLM#707)

* Harden layer package stage preparation

* Reduce stage control handler complexity

* Fix split runtime partial-load test mock

---------

Co-authored-by: James Dumay <jameswdumay@gmail.com>
* Use resumable hf-hub fork

* Use combined hf-hub branch
* Move CLI integrations into bundled plugins

* Support OpenCode JSONC config files

* Extract blackboard plugin crate

* Stage bundled plugin binaries in CI smokes

* Stop shipping blackboard with mesh-llm

* Make PR Linux tests tolerate sccache cache outages

* Drop unnecessary workflow and build-script changes
Complete Skippy shared-prefix record candidates (Mesh-LLM#694)

Fixes the Skippy prefix-cache record site for issue Mesh-LLM#683 so shared-prefix agent prompts can produce reusable cache candidates instead of repeatedly missing on the common “same long prefix, different tail” workload.

Validation
* Validation tier: Tier 2 - narrow Skippy prefix-cache record-site repair for issue Mesh-LLM#683, refreshed on current main.
* git fetch --no-tags origin main:refs/remotes/origin/main: PASS, origin/main at 6669b41.
* git rebase origin/main: PASS
* git diff --check origin/main...HEAD: PASS, no output
* git diff --check: PASS, no output
* git diff --cached --check: PASS, no output
* cargo fmt --all -- --check: PASS
* cargo test -p skippy-cache --lib: PASS, 31 passed
* cargo test -p skippy-server prefix --lib: PASS, 7 passed
* cargo test -p skippy-server resident_activation --lib: PASS, 4 passed
* cargo test -p skippy-server --lib: PASS, 104 passed
* cargo check -p mesh-llm: PASS
* cargo-clippy clippy -p skippy-server --all-targets -- -D warnings: PASS
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - cache behavior repair only; no release/version sync required.
* Not run: live shared-prefix agent smoke - no local model/runtime endpoint was available; deterministic candidate-planning and cache unit tests cover the changed paths.

Rollback
* git revert HEAD
Refresh Skippy package preflight diagnostics (Mesh-LLM#703)

Validation
* Validation tier: Tier 2 - narrow skippy-model-package CLI diagnostics refresh for issue Mesh-LLM#630 package-preflight support after rebasing onto current main.
* git fetch --no-tags origin main:refs/remotes/origin/main: PASS, origin/main at 4f52c43.
* git fetch --no-tags fork codex/skippy-split-package-preflight:refs/remotes/fork/codex/skippy-split-package-preflight: PASS.
* git rebase origin/main: PASS.
* git diff --check origin/main...HEAD: PASS, no output.
* git diff --check: PASS, no output.
* git diff --cached --check: PASS, no output.
* cargo fmt --all -- --check: PASS.
* cargo test -p skippy-model-package preflight: PASS, 6 passed, 10 filtered out.
* cargo test -p skippy-model-package --bin skippy-model-package: PASS, 16 passed.
* cargo check -p skippy-model-package --bin skippy-model-package: PASS.
* cargo-clippy clippy -p skippy-model-package --all-targets -- -D warnings: PASS.
* Remote CI on final head 506fe47: PASS.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - package diagnostics/tooling only; no release/version sync required.
* Not run: live HF package/direct split startup smoke - not required for this package preflight PR; deterministic manifest/artifact/stage diagnostics are covered by targeted unit tests.

Rollback
* git revert HEAD
i386 and others added 22 commits July 11, 2026 08:31
…s active (Mesh-LLM#952)

* fix: gate passive inference streams behind admission when a trust policy is active

stream_allowed_before_admission exempted STREAM_TUNNEL_HTTP (0x04) and
STREAM_ROUTE_REQUEST (0x05) from the quarantine gate unconditionally, so a
caller holding a leaked invite token could route inference through a node
even after the trust gate rejected it at gossip (e.g. UntrustedOwner under
TrustPolicy::Allowlist). The token was a bearer credential for inference
regardless of the node's admission policy.

The exemption now applies only under TrustPolicy::Off (the default open-mesh
posture — no behavior change there). With PreferOwned/RequireOwned/Allowlist,
only STREAM_GOSSIP bypasses the gate: if a node enforces who may join, the
same enforcement covers who may consume.

Found by an embedded-SDK admission harness (allowlist serve node + trusted
client + stranger client reusing the trusted invite token): the stranger was
rejected at gossip but still completed a chat completion via 0x04.

New regression test: passive_streams_are_gated_when_trust_policy_active.

* fix: keep prefer-owned passive access advisory

---------

Co-authored-by: James Dumay <jameswdumay@gmail.com>
* fix(tui): indicate clipped join tokens

* test(tui): cover empty join token slices
)

* Document architecture and model packages

* docs: address architecture docs review feedback
* Improve plugin documentation

* Address plugin documentation review feedback
A node with a verified owner identity started a second iroh endpoint (the
owner-control listener, ALPN mesh-llm-control/1) using the SAME secret key as
the main mesh endpoint, so both endpoints presented the SAME iroh endpoint id.
The control protocol requires this: it validates the dialed target_node_id
against the main endpoint id (verify_control_plane_target_node), so the control
endpoint must present that id.

But an iroh relay keeps only ONE active connection per endpoint id. The control
listener enabled relay and bound AFTER the main mesh endpoint, so it evicted the
main endpoint's relay registration (relay: 'Another endpoint connected with the
same endpoint id. No more messages will be received.'). The main mesh endpoint
then went dark on the relay: all relay-delivered mesh traffic (gossip, joins,
inference routing) stopped, so any peer that could not reach the node directly
never completed its join, never synced the model catalog, and every inference
returned 429/503. This is the consume-side 'connects but never joins' failure.

Fix: bind the owner-control listener with RelayMode::Disabled so it never
contends for the shared id's relay slot, leaving the main mesh endpoint as the
sole relay registrant. The main mesh endpoint is unchanged and keeps relay/NAT
traversal. Also clear_ip_transports() on the control endpoint so an explicit
IPv4 bind does not leave iroh's implicit [::]:0 socket (which can trip
MultipathNotNegotiated with relay disabled), matching the main endpoint's
LAN-only handling.

Trade-off: owner-control is now reachable over its direct/advertised address
only; relay-assisted remote owner-control across NAT is not supported while the
control and mesh endpoints share one id. Giving control its own relay presence
would require a distinct id, which the control target validation forbids.

Regression test: control_plane_listener_token_carries_no_relay_urls asserts the
control token advertises zero relay URLs while keeping the main endpoint id.

Empirically: with owner identity, a cross-machine consumer never joined; with
this fix (proven via owner-suppressed A/B) it joins over the relay and inference
returns HTTP 200.
Signed-off-by: ccarpene-blk <ccarpene@block.xyz>

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zizmor found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep OSS found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Signed-off-by: ccarpene-blk <ccarpene@block.xyz>
@ccarpene-blk ccarpene-blk reopened this Jul 30, 2026
Signed-off-by: ccarpene-blk <ccarpene@block.xyz>
@ccarpene-blk ccarpene-blk changed the title fix(mesh): restrict peer HTTP tunnels to inference routes fix(mesh): add inference-only embedded peer surface Jul 30, 2026
Signed-off-by: ccarpene-blk <ccarpene@block.xyz>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants