Commit Graph
151 Commits
Author SHA1 Message Date
tlongwell-blockandDawn d1262beb8a mesh-llm: e2e tests against a live relay (#[ignore]'d by default)
crates/sprout-test-client/tests/e2e_mesh_llm_discovery.rs (new): four
end-to-end tests verifying the kind:31990 publish/discover loop against
a real running sprout-relay. Follows the same #[ignore]+RELAY_URL
pattern as e2e_long_form.rs etc.

Tests:
- test_offer_publish_then_retrieve: publish a kind:31990 with a
  far-future expires_at, REQ it back via kinds+author filter, confirm
  the serialised MeshLlmOffer round-trips through the relay intact
  (expires_at, d_tag).
- test_offer_replace_by_d_tag: NIP-33 replace semantics — publishing
  a second event with the same (pubkey, d_tag) must replace the first;
  a subsequent REQ returns only the latest.
- test_offer_delete_by_empty_replace: the desktop publisher's
  delete-by-replace tombstone — publish a real offer, then publish
  empty content at the same address; only the tombstone remains.
- test_offer_stray_h_tag_is_ignored: kind:31990 is global
  (is_global_only_kind); a stray h-tag must not channel-scope the
  event. Verifies the unit-tested behaviour holds end-to-end on
  real relay traffic.

These exercise the slice that pure unit tests can't reach: the actual
NIP-43 fan-out gate, the relay's ingest validation against
required_scope_for_kind / is_global_only_kind, and NIP-33 storage
semantics. They are #[ignore]'d so 'just test-unit' is unaffected;
'cargo test --test e2e_mesh_llm_discovery -- --ignored' runs them
against the relay pointed at by RELAY_URL (default ws://localhost:3000).

clippy + fmt clean across the 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>
2026-05-19 17:27:13 -04:00
tlongwell-blockandDawn f711e37a7a mesh-llm: Max review fixups — expires_at, EndpointAddr, same-relay filter
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>
2026-05-19 17:20:29 -04:00
tlongwell-blockandDawn 1dbc89cc11 mesh-llm: offer envelope (sprout-core) + desktop building blocks
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>
2026-05-19 16:55:45 -04:00
tlongwell-blockandDawn db6c1103aa iroh-relay: wire spawn() into fn main with graceful shutdown
- IrohRelayHandle::shutdown() consumes self and awaits Server::shutdown()
  (which drains in-flight QUIC sessions before returning), replacing the
  previous drop-aborts-supervisor pattern.

- main.rs::serve() now starts the embedded iroh-relay when *both*
  SPROUT_IROH_RELAY_PUBLIC_URL and SPROUT_IROH_RELAY_BIND_ADDR are set.
  Spawned alongside the HTTP listener; subscribed to the same shutdown_tx
  watcher so SIGTERM/Ctrl-C drains the iroh-relay together with axum.

- Mismatched config logs a warn! and starts neither:
    * URL only  -> NIP-11 advertises a phantom endpoint -> mesh-LLM is broken
    * bind only -> clients can't build the NIP-98 'u' tag -> 100% denial
  In both cases we fail loud and refuse to lie to clients.

- Both serve() paths (UDS-enabled + TCP-only) await the iroh drain task
  before returning so shutdown is actually graceful end-to-end.

sprout-relay --lib: 208/208 unit tests pass (unchanged; this is a wiring
change, not an auth change).
workspace clippy -D warnings: clean.
workspace 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>
2026-05-19 16:45:31 -04:00
tlongwell-blockandDawn 2d818b6efd iroh-relay: review fixups from Mari + Max
Mari (trust):
- Add 64 KiB pre-decode length cap on the bearer token. NIP-98 events are
  well under a kilobyte; rejecting oversized inputs before allocating the
  base64 decode buffer prevents an admission request from coercing the
  relay into multi-megabyte allocations. New const MAX_BEARER_LEN.
- New verify_bearer_rejects_oversized_token test.
- New verify_bearer_rejects_internal_whitespace test: pins the fact that
  base64 0.22's general_purpose engines reject mid-token whitespace
  (no MIME mode), which is what we want.

Max (review):
- Soften the module-level 'patched-fork hooks' docs so they don't imply
  the per-client max-lifetime hook is already wired, and add an explicit
  TODO(patched-iroh-relay) marker at the future insertion site in spawn.
- Add SPROUT_IROH_RELAY_BIND_ADDR to Config (iroh_relay_bind_addr:
  Option<SocketAddr>) now, so the main.rs wiring follow-up can read it
  without a separate config churn. Server::spawn owns its own listener,
  so this is independent of the Sprout HTTP bind_addr.

sprout-relay --lib: 206 -> 208 tests pass (+2).
workspace clippy -D warnings: clean.
workspace 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>
2026-05-19 15:46:31 -04:00
tlongwell-blockandDawn 3ca6918078 mesh-llm plan v6.1: Step 3 — embedded iroh-relay with NIP-98 admission
- New module crates/sprout-relay/src/iroh_relay.rs (~290 lines incl. tests).
- pub fn spawn(state, bind_addr) constructs an iroh_relay::server::Server
  with AccessConfig::Restricted set to a closure that:
    1. Pulls the Bearer token from ClientRequest::auth_token().
    2. base64-decodes (accepts STANDARD + URL_SAFE, padded or not).
    3. Calls sprout_auth::verify_nip98_event against canonical URL
       (= sprout_auth::nip98_canonical_url(public_url, '/relay')).
    4. Runs check_relay_membership against the NIP-98 pubkey.
       Anything other than Member/ViaOwner/OpenRelay -> Deny.
  Per Max's review notes: fail-closed on missing/invalid token, run
  membership only after NIP-98 verifies the pubkey, no caching.
- Returns Ok(None) gracefully when SPROUT_IROH_RELAY_PUBLIC_URL is unset
  (the canonical URL can't be built without it).
- patched-iroh-relay feature flag reserved for upstream PR C's per-client
  max-lifetime hook (kept behind cfg so unpatched rc.0 still compiles).

- MSRV bumped from 1.88.0 -> 1.91.0 (iroh-relay rc.0's MSRV). Repo's
  rust-toolchain.toml already pins 1.95.0 so builds are unaffected; the
  bump just keeps Cargo.toml honest with the actual transitive floor.
- README updated: 'Rust 1.88+' -> 'Rust 1.91+'.
- crates/sprout-relay/Cargo.toml: added
  iroh-relay = { version = "=1.0.0-rc.0", features = ["server"] }
  plus the patched-iroh-relay feature.

Tests (rustc 1.95, via rust-toolchain.toml; also verified independently
on 1.91.1):
- sprout-relay --lib: 195 -> 206 (+11 iroh_relay tests covering valid
  admission, missing/empty/non-base64/wrong-method/wrong-URL/wrong-kind/
  stale-timestamp denials, and bearer-encoding round-trips).
- cargo clippy --workspace --all-targets -- -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>
2026-05-19 15:39:35 -04:00
tlongwell-blockandDawn f4ca099b1c mesh-llm plan v6.1: Steps 1, 2, 4, 5
- Step 1: KIND_MESH_LLM_DISCOVERY = 31990 (parameterized replaceable,
  global-only, MessagesWrite scope) — relay members announce compute offers
  through the same NIP-43-gated fan-out path as messages.

- Step 2: extract transport-neutral check_relay_membership returning
  MembershipDecision. HTTP enforce_relay_membership becomes a thin
  wrapper that maps Denied -> 403 JSON. Same behavior for all 6 existing
  HTTP callers; non-HTTP gates (iroh-relay AccessConfig) can call the
  core directly without a StatusCode in their return type.

- Step 4: NIP-11 iroh_relay_url field, fed from new
  SPROUT_IROH_RELAY_PUBLIC_URL config. Absent unless configured (older
  clients unaffected). This is what mesh-llm sidecars read to wire their
  iroh endpoints to Sprout's own relay -- no out-of-band config required.

- Step 5: sprout-auth::nip98_canonical_url helper. Single source of truth
  for the NIP-98 'u'-tag value, used by both signer and verifier. Suffix-
  aware path join (preserves /iroh prefix when joining /relay), localhost/
  IPv6 loopback collapse, query+fragment stripping. Round-trip test signs
  with the helper and verifies through verify_nip98_event to prevent drift.

sprout-core: 165 tests pass
sprout-auth: 36 -> 48 tests pass (+12 nip98_url)
sprout-relay: 190 -> 195 tests pass (+3 mesh-llm, +2 iroh_relay_url)
workspace clippy -D warnings: clean
workspace 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>
2026-05-19 15:33:13 -04:00
tlongwell-blockandGitHub 24520aa2b1 NIP-AE: agent engrams (kind:30174) — core memory injection + sprout mem CLI (#593)
Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
2026-05-19 13:16:37 -04:00
tlongwell-blockandGitHub 6e1418e571 feat(huddle): fix concurrent-speaker mixing + jitter buffer + protocol v2 (up to 10 peers) (#609)
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-05-18 16:34:52 -04:00
tlongwell-blockandGitHub 70cb53e2c7 Add Sprig all-in-one agent binary (#605)
Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
2026-05-17 13:31:58 -04:00
tlongwell-blockandGitHub e4e9923799 feat(agent): add OpenAI Responses API with auto endpoint detection (#604)
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
2026-05-16 21:23:33 -04:00
tlongwell-blockandGitHub 92c859043c dev-mcp: add view_image tool (#602)
Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
2026-05-15 16:21:16 -04:00
tlongwell-blockandGitHub 7171d4954d fix(relay,desktop): only advertise NIP-43 when enforced; probe pairing by supported_nips (#601)
Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
2026-05-15 15:11:37 -04:00
tlongwell-blockandGitHub 9e94a7c475 fix(agent): fix OpenAI-compat request body serialization and max_tokens (#595) 2026-05-15 11:07:14 -04:00
Will PflegerandGitHub 1d8a130b32 refactor(cli): restructure flat commands into 12 subcommand groups (#585) 2026-05-14 19:13:49 -04:00
Will PflegerandGitHub 01a3df7f1f feat(sdk): add builder functions for workflows, DMs, and presence (#589) 2026-05-14 18:49:57 -04:00
1b87a0905f refactor: extract shared @mention resolver into sprout-sdk (#580)
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Dawn <dawn@sprout.local>
2026-05-14 11:38:52 -04:00
2ee7356424 fix: add default-run to sprout-relay so cargo run -p sprout-relay works (#577)
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 08:34:23 -07:00
tlongwell-blockandGitHub 9a403d3e8a fix: channel member search via NIP-50 + Typesense indexer fix for kind:0 (#569)
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
2026-05-13 15:03:35 -04:00
tlongwell-blockandGitHub 8d5abb7a09 chore(acp): raise default idle timeout from 320s to 620s (#566)
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
2026-05-13 11:04:53 -04:00
tlongwell-blockandGitHub e427581ca3 fix(cli): derive thread root from parent event tags (#564)
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-05-13 09:54:43 -04:00
tlongwell-blockandGitHub 90063cf5f0 fix: skip empty assistant turns instead of placeholder space (#560) 2026-05-13 09:40:41 -04:00
80b79c4f75 agent: raise default SPROUT_AGENT_MAX_OUTPUT_TOKENS to 32768 (#565)
Co-authored-by: Tyler Longwell <tlongwell-block@users.noreply.github.com>
2026-05-13 13:03:56 +00:00
tlongwell-blockandGitHub cacfe6b84e fix(nip11): advertise auth_required: true to match actual enforcement (#556)
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
2026-05-12 20:08:53 -04:00
tlongwell-blockandGitHub 69734bff00 fix: Anthropic rejects empty text blocks in assistant history (#559) 2026-05-12 19:41:50 -04:00
tlongwell-blockandGitHub a33d73bf6d agent: make max_sessions default unlimited (#547) 2026-05-12 17:32:19 -04:00
tlongwell-blockandGitHub 08e00a6f0a fix(relay): auto-create git_repo_path so kind:30617 init can't silently fail (#545) 2026-05-12 16:40:38 -04:00
tlongwell-blockandGitHub 385e1716b7 agent+acp: preserve session on retriable agent errors (#544) 2026-05-12 13:57:02 -04:00
tlongwell-blockandGitHub 6b4f9f05d4 cli: add display name lookup to get-users (#543) 2026-05-11 20:41:02 -04:00
tlongwell-blockandGitHub 23f1637728 fix: classify agent JSON-RPC errors as application errors, not transport (#539) 2026-05-11 17:44:34 -04:00
tlongwell-blockandGitHub d953d0ecb4 fix: default max_rounds to 0 (unlimited) (#540) 2026-05-11 20:09:25 +00:00
WesandGitHub 1d604e8b2e Update Rust dependencies for pair relay (#534) 2026-05-11 10:47:22 -07:00
tlongwell-blockandGitHub 98030c526a feat: Git repository management commands (#533) 2026-05-11 13:40:51 -04:00
tlongwell-blockandGitHub 120b1b0c06 Add sprout-agent avatar image (#530) 2026-05-11 17:05:35 +00:00
tlongwell-blockandGitHub 22f1e0b724 Launch sprout-agent from the desktop GUI (#529) 2026-05-11 12:36:53 -04:00
tlongwell-blockandGitHub 70a691517b feat: automatic git auth and signing for sprout agents (#528) 2026-05-11 10:38:45 -04:00
tlongwell-blockandGitHub 91f16e326f feat: embed sprout CLI in dev-mcp, add NIP-OA auth tag support (#525) 2026-05-11 06:57:34 -04:00
tlongwell-blockandGitHub 082414bf3f feat: sprout-agent + sprout-dev-mcp — minimal ACP coding agent (#493) 2026-05-10 22:33:58 -04:00
tlongwell-blockandGitHub 32c3fd1340 fix: extract NIP-OA owner on open relays for observer frame auth (#513) 2026-05-08 16:00:23 -04:00
0f10861850 feat(desktop): expand default agent toolsets with canvas, forums, dms, and member list (#510)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 10:10:06 -07:00
0dcfa86f84 chore: remove dead API token references from GUI, docs, and config (#498)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 19:07:44 +00:00
tlongwell-blockandGitHub 1f1f8740e7 feat: materialize agent_owner_pubkey on NIP-OA auth (#491) 2026-05-06 16:08:21 +00:00
tlongwell-blockandGitHub 2357c3ddb3 fix: unify NIP-OA relay membership enforcement across all ingress paths (#490) 2026-05-06 11:01:39 -04:00
tlongwell-blockandGitHub ceb5afd2d4 fix(relay): raise MAX_HISTORICAL_LIMIT from 500 to 10,000 (#486) 2026-05-05 20:44:29 +00:00
3a12369491 feat: serve web UI from relay + repos page redesign (#479)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 19:20:13 +00:00
tlongwell-blockandGitHub 4701bd6f32 fix(relay): include p tags in kind:39000 for DM channels (#482) 2026-05-05 19:10:52 +00:00
ea62664759 post-475 fixes (#481)
Co-authored-by: Wes <wesb@block.xyz>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:41:18 -04:00
tlongwell-blockandGitHub 454781caae Sprout speaks Nostr. Nothing else. (#475) 2026-05-05 12:03:06 -04:00
tlongwell-blockandGitHub df33818b5b feat(relay): NIP-OA agent authentication for NIP-43 membership (WS + REST + git) (#471) 2026-05-04 12:12:22 -04:00
tlongwell-blockandGitHub 15bd9ba2ac feat: sprout-pair-relay — ephemeral sidecar for NIP-AB device pairing (#467) 2026-05-03 21:07:36 -04:00