The verifier side of the iroh-relay NIP-98 admission is fully wired and
tested in sprout-relay::iroh_relay. The client side (build_nip98_bearer)
is reserved for the deferred dial path — it's complete and unit-tested
on its own, but has no live caller until upstream mesh-llm PR A lands.
Adds a module-level #![allow(dead_code)] with a comment pointing at the
deferred-dial dependency, so the workspace stays warning-clean without
losing the test coverage on the helper.
cargo clippy --workspace -- -D warnings + cargo fmt --check clean.
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Per Max's pre-PR review of d9a791f:
1. Add MeshLlmOffer.expires_at: u64 (unix seconds). Hard-required by
serde (no default) since consumers depend on it for correctness:
crashed publishers cannot send the NIP-33 delete-by-replace
tombstone, so the TTL is the only thing that reaps stale offers.
New helpers + tests in sprout-core::mesh_llm:
- is_expired(now) returns true when expires_at <= now.
- matches_local_relay(current_relay) compares against the relay's
NIP-11 iroh_relay_url with lightweight canonicalisation
(lower-case scheme/host, trailing-slash collapse, query/fragment
drop). v1 invariant: one relay = one mesh boundary.
- is_publishable now rejects expires_at == 0.
- canonical_relay_url() is a small private helper, deliberately
separate from sprout_auth::nip98_canonical_url (different jobs).
5 new tests (expires_at_required, is_expired_filter,
matches_local_relay_canonicalises, is_publishable_rejects_zero_*,
plus updated JSON fixtures throughout).
2. Fix doc wording: iroh NodeAddr -> EndpointAddr (rc.0 naming).
Soften 'multiple relays bridging into membership scope' language to
reflect that v1 is strictly single-relay and the field is reserved
for future cross-relay only.
3. Desktop publisher (mesh_publish_offer) now computes
expires_at = now + OFFER_TTL_SECS (15 min) and threads it through
build_offer(endpoint_id, iroh_relay_url, expires_at). The frontend
hook is expected to re-invoke publish on heartbeat well before the
deadline (heartbeat plumbing is in the next commit).
4. Frontend useMeshLlmOffers hook:
- probes the relay's NIP-11 iroh_relay_url once on mount and
filters offers whose iroh_relay_url doesn't match (v1 same-relay
invariant). When the relay doesn't advertise mesh-LLM, the filter
correctly empties the list.
- rejects events with schema v != 1 before storing.
- 30s setInterval ticks 'now' so expired offers drop without waiting
for a new event. O(1) timer regardless of how many offers are in
the cache.
- mirrors the canonicaliser logic from sprout-core in TS so JS-side
accept/reject decisions match the Rust check.
Tests after change:
- sprout-core --lib: 174 -> 178 (+4 mesh_llm)
- desktop mesh_llm --lib: 11 unchanged (publisher signature change
required updating build_offer call sites; tests pass exact-same set).
- pnpm typecheck + pnpm check (biome + file-sizes): clean.
- cargo clippy --workspace -- -D warnings: clean.
- cargo fmt --all -- --check: clean.
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
desktop/src-tauri/src/commands/mesh_llm.rs: new mesh_publish_offer
command. Reads persisted prefs and the local iroh endpoint id; on
enabled=true, builds a kind:31990 event with the JSON-serialised
MeshLlmOffer envelope and a 'd' tag matching prefs.d_tag, then signs
+ POSTs via the existing submit_event pipeline (NIP-98 to /events).
On enabled=false, publishes the *same address* with empty content —
NIP-33's 'delete by replace' idiom — so consumers know the offer has
been withdrawn. PublishOfferResult.published_offer reports which path
was taken.
desktop/src/features/settings/ui/MeshComputeSettingsCard.tsx: persist()
now follows save-prefs with a relay capability probe and (when the
relay advertises iroh_relay_url) a publish call. If the relay doesn't
support mesh-LLM, prefs are still saved locally and the UI surfaces a
specific 'this relay does not advertise iroh_relay_url' message rather
than a confusing 'publish failed'.
The user-facing flow is now: open settings -> Share compute -> toggle
on -> a kind:31990 event hits the relay, NIP-43-fanned-out to other
members. Toggling off publishes the empty-content replacement.
Tests: 208 sprout-relay, 174 sprout-core, 11 desktop mesh_llm — all
unchanged-and-pass. desktop typecheck + biome clean.
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
desktop/src-tauri/src/commands/mesh_llm.rs (new):
- mesh_get_endpoint_id(app) -> { endpoint_id }: creates the persisted
iroh keypair on first call; returns the canonical Display form of
the public key so the UI can show 'this device' identity.
- mesh_get_sharing_prefs(app) -> ComputeSharingPrefs: reads the
persisted prefs file (defaults applied when absent).
- mesh_set_sharing_prefs(app, prefs) -> (): atomic write-through.
- mesh_relay_iroh_url(state, relay_ws_url) -> Option<String>: probes
the relay's NIP-11 doc for iroh_relay_url; returns None gracefully
when the relay doesn't advertise mesh-LLM.
All four registered in lib.rs's tauri::generate_handler! block.
Frontend hooks land in C1 (avatar-menu MeshComputeSettingsCard).
Cleanup: drop wildcard re-exports from mesh_llm/mod.rs so unused
publisher/dialer helpers don't trip top-level dead-code lints before
B5/B6 are wired in.
cargo check passes; clippy + fmt clean (relay-side workspace).
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
desktop/src-tauri/src/mesh_llm/nip11.rs: fetch_iroh_relay_url(ws_url)
converts ws:// -> http://, GETs / with Accept: application/nostr+json,
extracts the iroh_relay_url field from the NIP-11 JSON. Returns
Ok(None) on unreachable relays / malformed responses / missing field
so mesh-LLM silently disables itself rather than producing a deploy
mystery; only returns Err for un-fixable caller mistakes (non-ws URL).
Mirrors the probe_relay_supports_nip43 helper already in
commands/pairing.rs; deliberately doesn't share code since this is a
different decode shape with different graceful-failure semantics.
desktop mesh_llm tests: 9 -> 11 (+2 nip11 helper).
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
B0 — sprout-core/src/mesh_llm.rs (new): MeshLlmOffer envelope, the
content of a kind:31990 event. Schema versioned (v: u32), with
deny_unknown_fields at the top level and a freeform 'extra' Value
escape hatch. ResourceCaps + ModelOffer sub-structs. d_tag charset is
limited to [A-Za-z0-9_-] (NIP-33 stability). 9 unit tests covering
round-trip JSON, optional caps, unknown-field rejection, d_tag
validation, is_publishable rule set.
B2 — desktop/src-tauri/src/mesh_llm/endpoint.rs: persists the iroh
endpoint keypair to {app_data_dir}/mesh_iroh.key as 32 hex bytes.
Atomic write via tempfile.persist; corrupt files quarantined to
.bad.{epoch} (same pattern as identity.key). 2 unit tests.
Design note in the module doc: we deliberately do NOT derive the
iroh key from the Nostr key, because that would couple key rotation
(rotating the Nostr key would silently break active offers) and
invent a new key-custody convention. Separate file, same Tauri
sandbox.
B3 — desktop/src-tauri/src/mesh_llm/nip98.rs: build_nip98_bearer(keys,
iroh_relay_public_url) signs a kind:27235 event with the user's Nostr
key over the canonical relay URL (sprout_auth::nip98_canonical_url
with path '/relay'), base64-encodes the event JSON. This is the exact
token the relay's iroh_relay::verify_bearer decodes + verifies.
3 unit tests.
B-offer prefs — desktop/src-tauri/src/mesh_llm/offer.rs: persisted
ComputeSharingPrefs (the avatar-menu sliders). Default is disabled,
1 concurrent consumer cap. build_offer() returns None when disabled
so callers know to *delete* any prior offer rather than re-publish.
JSON round-trip + helper tests, 4 tests.
Workspace deps added:
- desktop pulls sprout-auth (for the canonical URL helper, nostr-free
at the API surface so the 0.36/0.37 nostr split doesn't matter).
- desktop pulls iroh-base = =1.0.0-rc.0 with the 'key' feature for
SecretKey/PublicKey/EndpointId.
- desktop pulls thiserror = '2'.
Tests: 9 desktop mesh_llm tests pass + 9 sprout-core mesh_llm tests
pass. Workspace clippy + fmt clean (relay side; desktop has expected
dead_code warnings until B4-B6 wire these in).
Note: desktop crate requires sidecar binary stubs in
desktop/src-tauri/binaries/ to typecheck; created via the existing
'just _ensure-sidecar-stubs' helper.
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>