feat: run mesh as a library, and join gated relays - #641
Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for per-relay bearer authentication so the runtime can register with restricted (gated) iroh relays while continuing to use unauthenticated public relays.
Changes:
- Introduces
--relay-auth URL=TOKENCLI parsing and wiring through runtime startup. - Extends mesh endpoint construction to attach auth tokens to specific relay configs when building an
iroh::RelayMap. - Updates mesh tests and adds focused unit tests for relay-auth parsing and relay-map token attachment behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| crates/mesh-llm-host-runtime/src/runtime/mod.rs | Builds a relay-auth map from CLI args and passes it into mesh::Node::start. |
| crates/mesh-llm-host-runtime/src/mesh/mod.rs | Adds per-relay auth token support in relay map construction and threads it into endpoint/control listener setup; adds unit tests. |
| crates/mesh-llm-host-runtime/src/mesh/tests.rs | Updates control-listener test call sites for the new relay_auths parameter. |
| crates/mesh-llm-host-runtime/src/cli/mod.rs | Adds --relay-auth flag with a URL=TOKEN parser plus parser unit tests. |
Comments suppressed due to low confidence (1)
crates/mesh-llm-host-runtime/src/mesh/mod.rs:277
- Rustdoc link
[RelayMap]likely won't resolve here becauseRelayMapisn't in scope (the function returnsiroh::RelayMapvia a fully-qualified path). Consider changing the link target to[iroh::RelayMap](or plain backticks) to avoid broken intra-doc links/warnings.
fn effective_relay_urls(relay_urls: &[String]) -> Vec<String> {
if relay_urls.is_empty() {
vec![
"https://usw1-2.relay.michaelneale.mesh-llm.iroh.link./".into(),
"https://aps1-1.relay.michaelneale.mesh-llm.iroh.link./".into(),
|
Found one more thing: Medium: --relay-auth fails before serve / client in split-arg form Example: Suggest: Add "--relay-auth" to value_taking_flags and add parser tests for |
… --relay-auth before serve/client PR feedback fixes for #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.
|
Thanks both — pushed @ndizazzo — fixed. Added
@copilot — dropped the broken While here, added a real defence for the feature too — an in-process iroh-relay with
These exercise the actual wire path through |
|
Pushed
mesh_llm::sdk::RuntimeBuilder::new()
.client(true)
.auto(true)
.relay("https://gated.example/")
.relay_auth("https://gated.example/", "<nip98-bearer>")
.run()
.await?;How it worksThe builder collects fields, serialises them to argv, and feeds that argv to the same One code path, two surfaces. The CLI parser is the same parser the SDK targets. What it coversThe realistic CLI surface: Known limitations (documented in the module)
Tests7 unit tests pin each setter's argv. The decisive one is Stability statement
|
Follow-up work to planPR is in draft. Notes here on the next steps that this PR clears the runway for. Whether any of these land in this PR or a follow-up is open \u2014 captured here so we don't lose context either way. 1. Make
|
Copilot review pass — addressedReviewers added: @ndizazzo, @i386. Walked Copilot's two review passes: Live finding (fixed in
Added Stale findings (ignore): Three comments in Copilot's second pass refer to First-pass intra-doc link |
Copilot review pass 3 \u2014 addressedSeven comments, all the same shape: Verdict: harmless today (compiles, tests pass on every CI lane), because the callee only borrows the reference synchronously before its internal But the pattern is fragile \u2014 if the callee's signature ever shifts the borrow across an await, every test would break at once. Cheap future-proofing, applied in Net diff is slightly smaller ( |
i386
left a comment
There was a problem hiding this comment.
Code review pass: two host-runtime SDK lifecycle issues to address before merge.
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.
…art under arg limit
… --relay-auth before serve/client PR feedback fixes for #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.
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.
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.
…ime feature
Lets a Rust app drive a real mesh-llm node from the published SDK \u2014 the
same iroh-backed peer the binary runs, not the HTTP-shim client that
MeshNode::start() used previously.
```rust
let node = MeshNode::builder()
.identity(OwnerKeypair::generate())
.join(invite)
.role(MeshRole::Client)
.relay("https://gated.example/")
.relay_auth("https://gated.example/", "<bearer-token>")
.max_vram_gb(0.0)
.build()?;
node.start().await?;
let invite = node.invite_token().await;
```
Architecture:
- New `mesh_llm_host_runtime::host_node` module exposes `HostNodeSpec`
+ `HostNode` + `start_host_node` as the curated entry point into the
internal `mesh::Node`. SDK consumers don't see internal types directly.
- `mesh-llm-api-server` gains a `host-runtime` Cargo feature
(off by default). With it on, depends on `mesh-llm-host-runtime` and
`MeshNode::start()` calls `host_node::start_host_node` with the
builder's relay / relay_auth / role / quic_bind / max_vram fields.
- Builder API extended with .role(), .relay(), .relay_auth(),
.quic_bind(), .max_vram_gb(), .no_enumerate_host(). New types
`MeshRole` and `MeshQuicBind` mirror the CLI surface; SDK consumers
never have to import host-runtime-internal types.
- Without the feature, the new builder methods are still callable
(forward-compat: an SDK consumer can configure relay-auth without
caring whether the runtime is wired) \u2014 fields are stored but
ignored, and start() falls back to the existing HTTP-shim behaviour.
- New invite_token() / set_display_name() accessors on MeshNode
(host-runtime-only) so consumers can introspect / advertise their
running node.
Cycle fix: `mesh-llm-host-runtime` had an unused declared dep on
`mesh-llm-api-server` (no `use` sites in src/). Dropped to let the
inverse dep land cleanly.
Test: crates/mesh-llm-api-server/tests/host_node_gated_relay.rs
(gated on host-runtime feature) brings up an in-process iroh-relay with
AccessConfig::Restricted, builds a MeshNode with .relay_auth(...) for
the matching token, and asserts node.start() reaches the gated relay
end-to-end. A second test pins that the wrong token is denied with
'not authorized' at the iroh wire layer.
19f3ecf to
7c42372
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
crates/mesh-llm-api-server/Cargo.toml:43
base64andserde_jsonare added as dev-dependencies here, but there are no references to either incrates/mesh-llm-api-server(including the new host-runtime tests). Consider removing them to avoid unnecessary dependency bloat/compile time.
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread", "time"] }
# In-process iroh-relay for testing the host-runtime feature: real
# AccessConfig::Restricted relay, real endpoint bind, real WebSocket
# upgrade. Same dev-dep iroh feature combo `mesh-llm-host-runtime`
# already uses for its own gated-relay tests.
iroh = { version = "1.0.0-rc.0", features = ["test-utils"] }
iroh-relay = { version = "1.0.0-rc.0", features = ["server", "test-utils"] }
futures-util = "0.3"
base64 = "0.22"
serde_json = "1"
SDK consumers can now launch the full mesh node *including* the OpenAI
HTTP API surface from Rust code, without going through the binary CLI:
```rust
let node = MeshNode::builder()
.identity(OwnerKeypair::generate())
.join(invite)
.role(MeshRole::Client)
.relay("https://gated.example/")
.relay_auth("https://gated.example/", "<bearer>")
.openai_port(0) // 0 = OS-assigned ephemeral port
.build()?;
node.start().await?;
let base = node.openai_base_url().await.unwrap();
// e.g. http://127.0.0.1:54321
// Hit /v1/chat/completions, /v1/models, /v1/responses there as usual.
```
Mechanism:
- New `mesh_llm_host_runtime::host_node::start_openai_proxy(node, port,
listen_all)` wraps the internal `network::openai::ingress::api_proxy`
with default empty target_rx (routing pulls remote peers dynamically
from `node.hosts_for_model()` at request time) and a background drain
for runtime-control messages (SDK consumers without local serving
have no one to handle Load/Unload control requests).
- `MeshNodeBuilder` gains `.openai_port(port)` and
`.openai_listen_all(bool)` setters. With the `host-runtime` feature on
and `openai_port` set, `MeshNode::start()` binds and spawns the proxy
alongside the mesh node; `MeshNode::stop()` aborts it.
- New `MeshNode::openai_base_url()` accessor returns the bound URL
after start (Some when the proxy is running, None otherwise).
End-to-end test `openai_proxy_binds_and_serves_v1_models_over_http`:
constructs a MeshNode via the SDK with .openai_port(0), GETs /v1/models
over real TCP, asserts 200 OK with a JSON body containing the `data`
field (OpenAI shape), then stops the node and asserts the port no
longer answers.
Scope clarification in host_node module docs: the "this does not do"
list shrinks; OpenAI proxy is no longer in it. Local model serving
still requires plugging an EmbeddedServingController, which is a
separate concern (client-only embedders don't need it because the
proxy routes to remote mesh peers).
Adds the missing piece: SDK consumers can now run *exactly* what the
mesh-llm binary runs \u2014 not a degraded subset. `run_serve(spec)`
constructs argv from a typed `MeshServeSpec` and feeds it to the same
`runtime::run_with_args` entry point the binary calls.
```rust
use mesh_llm_api_server::{run_serve, MeshServeSpec};
run_serve(MeshServeSpec {
client: true,
auto: true,
relays: vec!["https://gated.example/".into()],
relay_auths: [(
"https://gated.example/".to_string(),
"<bearer>".to_string(),
)].into_iter().collect(),
port: Some(9337),
console_port: Some(3131),
max_vram_gb: Some(0.0),
..Default::default()
}).await?;
```
That gets the full thing: auto-discovery, election, tunnel manager,
OpenAI proxy, management console, local model serving (when configured),
plugin host. Same code path as `mesh-llm serve` / `mesh-llm client`.
Mechanism:
- `runtime::run()` split into a thin env-driven entry and a new
`run_with_args(argv)` that takes a caller-supplied argv. Binary
unchanged \u2014 main() still calls run() which forwards std::env::args_os.
- `mesh_llm_host_runtime::run_with_args(argv)` re-exports it at the
crate root.
- `host_node::MeshServeSpec` covers the realistic CLI surface: client,
auto, publish, mesh_name, region, display_name, join, discover,
models, ggufs, mmproj, port, console_port, headless, blackboard,
relays, relay_auths, nostr_relays, bind_port, bind_ip, listen_all,
max_vram_gb, no_enumerate_host, config, owner_key, owner_required,
node_label, trust_owners, debug. Plus an extra_args escape hatch.
- `host_node::run_serve(spec)` serialises the spec to argv via
`MeshServeSpec::into_argv()` and calls `run_with_args`.
- `mesh-llm-api-server::{run_serve, MeshServeSpec}` re-exports them
through the published SDK crate (gated on the host-runtime feature).
Regression-catcher test `mesh_serve_spec_argv_parses_via_the_real_cli_parser`:
constructs a fully-populated MeshServeSpec, calls into_argv(), runs it
through `normalize_runtime_surface_args` + `Cli::try_parse_from` (the
real parser the binary uses), asserts every field round-trips. If a
future refactor renames a CLI flag, this fails immediately and points
at the drifted MeshServeSpec field.
This is what sprout (or any Rust app) actually needs to run a full
mesh-llm node from inside its own process. The earlier
`MeshNodeBuilder` + `start_openai_proxy` work remains useful for
finer-grained client-only embedders that don't want the whole runtime
machinery, but `run_serve` is the answer to 'I want my Rust app to do
exactly what `mesh-llm serve` does.'
Adds the missing 'how do I run mesh-llm from Rust?' answer in three places so it's discoverable however a consumer arrives: - `docs/SDK.md` gains a 'Run the full mesh-llm runtime from Rust (host-runtime feature)' section under Rust Usage. Explains the feature flag, contrasts with MeshNodeBuilder (fine-grained vs full-runtime), gives a complete relay-auth + OpenAI + console example, lists every MeshServeSpec field. - `crates/mesh-llm-api-server/README.md` gets a parallel section so consumers landing on the crate page (e.g. via docs.rs or crates.io) see the run_serve story without leaving the crate docs. - The `pub use` re-export of `run_serve` / `MeshServeSpec` in `mesh-llm-api-server/src/lib.rs` now carries a full rustdoc example with the same MeshServeSpec, so `cargo doc` surfaces it prominently. No code changes; documentation only.
| pub async fn start(&self) -> Result<(), MeshApiError> { | ||
| self.inner.client.lock().await.join().await | ||
| #[cfg(feature = "host-runtime")] | ||
| { | ||
| let spec = HostNodeSpec { | ||
| role: match self.inner.host_node_spec.role { | ||
| MeshRole::Client => HostNodeRole::Client, | ||
| MeshRole::Serve => HostNodeRole::default(), | ||
| }, | ||
| relays: self.inner.host_node_spec.relays.clone(), | ||
| relay_auths: self.inner.host_node_spec.relay_auths.clone(), | ||
| quic_bind: HostQuicBindSelection { | ||
| ip: self.inner.host_node_spec.quic_bind.ip, | ||
| port: self.inner.host_node_spec.quic_bind.port, | ||
| }, | ||
| max_vram_gb: self.inner.host_node_spec.max_vram_gb, | ||
| enumerate_host: self.inner.host_node_spec.enumerate_host, | ||
| }; | ||
| let node = | ||
| host_node::start_host_node(spec) | ||
| .await | ||
| .map_err(|err| MeshApiError::Serving { | ||
| message: format!("host node start failed: {err}"), | ||
| })?; | ||
| if let Err(err) = node.join(self.inner.config.invite_token.as_str()).await { | ||
| node.shutdown().await; | ||
| return Err(MeshApiError::Serving { | ||
| message: format!("host node join failed: {err}"), | ||
| }); | ||
| } | ||
| node.start_accepting(); | ||
|
|
||
| // Spin up the OpenAI HTTP proxy if the builder asked for one. | ||
| // Equivalent to `mesh-llm … --port <port>`. Routes inference | ||
| // requests to mesh peers serving the requested model. | ||
| if let Some(port) = self.inner.host_node_spec.openai_port { | ||
| let listen_all = self.inner.host_node_spec.openai_listen_all; | ||
| let handle = | ||
| mesh_llm_host_runtime::host_node::start_openai_proxy(&node, port, listen_all) | ||
| .await | ||
| .map_err(|err| MeshApiError::Serving { | ||
| message: format!("openai proxy bind failed: {err}"), | ||
| })?; | ||
| *self.inner.openai_proxy.lock().await = Some(handle); | ||
| } | ||
|
|
||
| *self.inner.host_node.lock().await = Some(node); | ||
| // Also flip the legacy HTTP-shim client's connected flag so | ||
| // status()/events() callers see a connected node. Harmless. | ||
| self.inner.client.lock().await.join().await | ||
| } |
There was a problem hiding this comment.
Mine found the same:
Medium — host node leaks when OpenAI proxy bind fails
crates/mesh-llm-api-server/src/node.rs:502-513
If .openai_port(...) is set to a port that is already in use or cannot be bound, start_host_node(), join(), and node.start_accepting() have already succeeded before start_openai_proxy(...) returns an error. Because self.inner.host_node is not stored yet, a later stop() has no handle to shut the node down, leaving QUIC/background tasks alive. Add cleanup on this error path.
…nicalisation
Six findings from Copilot's latest pass on the SDK surface. All real.
1. HostNodeSpec / MeshServeSpec leaked bearer tokens via Debug.
Both structs derived Debug, so any {:?} (panic, tracing, etc.)
would expose relay_auths token values. Now Debug is implemented
manually with a RedactedAuthMap helper that prints relay URLs
(public) but replaces token values with '<redacted N bytes>'.
2. relay_map_from_urls did raw-string auth lookup.
Logically equivalent URLs ('https://x.example' vs 'https://x.example/')
would silently miss the auth map and the gated relay would reject
the registration with no clear signal. Now both sides canonicalise
via iroh::RelayUrl::parse before the lookup. Misconfigured auth
keys surface as Err naming the offending key. New tests
auth_token_matches_canonicalised_url_with_or_without_trailing_slash
and malformed_relay_auth_key_surfaces_as_error pin the contract.
3. SDK gated-relay test anchor used bundled public relays.
Made tests internet-dependent and slow. anchor_on_relay() now
takes the relay URL explicitly (and optional auth token for
gated). Both SDK test files spin up an in-process relay and point
the anchor at it so the suite stays offline and deterministic.
4. Clap's value_parser leaked tokens in 'invalid value' errors.
--relay-auth used #[arg(value_parser = parse_relay_auth_pair)],
so Clap's default 'invalid value '...' for '--relay-auth''
message would echo the full URL=TOKEN input on parse failure.
Now Cli::relay_auth is an opaque Vec<String>; validation
happens post-parse via Cli::parse_relay_auths() which we
control and which already redacts properly. Production call
sites in runtime/mod.rs updated. Test fixtures updated to
match the new shape (still pin the URL=TOKEN string going in
and the (url, token) pair coming out).
5. MeshNode::start() not idempotent under host-runtime feature.
A second start() spawned a second iroh endpoint and OpenAI
proxy and orphaned the first (stop() only knew about the most
recent). Now early-returns if host_node is already Some. New
test start_is_idempotent_on_repeat_calls in
openai_proxy.rs pins the contract: second start() returns
without rebinding, base URL unchanged.
6. openai_proxy test leaked the anchor HostNode.
Test bound the anchor to '_anchor' and never shut it down,
leaking the iroh endpoint + accept loop into subsequent tests.
Now keeps the anchor named and explicitly calls
anchor.shutdown().await before returning.
|
not sure if I like this approach yet, need to think about how rust crate is pre-built and shared |
Follow-on to docs/design/RUST_NATIVE_SDK.md. Investigates what it would take for the mesh-llm shipped binary to consume mesh-llm-api-server the same way an external Rust app does, instead of reaching into host-runtime internals. Findings: - The user-facing CLI subcommands (discover, download, models, blackboard) already have direct SDK equivalents. The duplication is roughly 400 lines of host-runtime-internal access in crates/mesh-llm-host-runtime/src/cli/commands/*. - Three change groups: (1) domain commands route through SDK, (2) serve/client route through run_serve(MeshServeSpec) from PR #641, (3) auth either moves into the SDK or is explicitly marked binary-only. - Concrete cost: 400-1000 lines re-pointed at SDK calls, plus the auth decision. Not a 7K-line rewrite. CLI shell (clap parsing, output, TUI) stays as the binary's job. - Several CLI surfaces explicitly stay bespoke: update, model-prepare, benchmark, gpu enumerate, stop, http-to-management-api commands. Sequencing depends on landing #690, the gated-relay split, and #691 first. Without mesh-llm-api-server actually on crates.io, the 'binary uses the SDK' story is internal-only.
|
Superseded by two unrelated PRs split out of this one:
Closing this one in favour of the cleaner split. |
cargo fmt under edition 2024 sorts uppercase types alongside lowercase modules. Reorders imports in the SDK files cherry-picked from #641. No logic change.
Three small cleanups to make this branch green on a workspace that doesn't (yet) have the rest of #641's gated-relay polish: 1. skippy-ffi/build.rs: collapse the nested ifs in the tarball-URL fetch path into a single let-chain so clippy's collapsible-if doesn't fire. 2. crates/mesh-llm-host-runtime/src/host_node.rs: remove three tests that depend on helpers only present on the gated-relay PR (id_returns_bare_hex_endpoint_id needs the bare-hex HostNode::id() refactor; shutdown_closes_the_mesh_endpoint needs Node::endpoint_is_closed_for_tests; shutdown_releases_fixed_quic_bind depends on the shutdown polish that releases the QUIC bind cleanly). They come back once that work is on main. Also remove the helpers (free_local_udp_port, probe_quic_port_released) those tests pulled in. 3. The mesh_serve_spec_argv_parses_via_the_real_cli_parser test no longer asserts on cli.relay_auth (that field doesn't exist on this branch). Updated to use 'https://public.example/' instead of 'https://gated.example/' since gated-relay support isn't here yet.
* 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 #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.
Lets you run a full mesh node from the SDK, and also join a gated relay if you need to.
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.