feat(chromecast): publish receiver-facing IPv6 media tickets (tr-b5s) - #174
feat(chromecast): publish receiver-facing IPv6 media tickets (tr-b5s)#174jm2 wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Pull Request Overview
This Pull Request is not up to standards due to a critical misalignment between its metadata and the implementation. While the PR title and description specify extensive functional changes to Chromecast media handling (IPv6 support, protocol compatibility, and Rust source modifications), the provided diff contains only a new GitHub Actions workflow configuration. None of the functional logic or fixes described are present in the code.
Additionally, the proposed GitHub Actions workflow contains high-risk issues, including a syntax error in tool authorization that will prevent command execution and the use of mutable tags for third-party actions, which poses a supply chain security risk. These blockers must be addressed by aligning the code with the PR intent and fixing the workflow logic.
About this PR
- Critical scope misalignment: The PR description and title specify extensive functional changes to Chromecast media handling, but the actual diff contains only a new GitHub Actions workflow configuration. The logic described is entirely missing from the implementation.
Test suggestions
- Verify IPv6 media ticket publishing on reachable scoped addresses
- Verify fail-closed behavior for unusable endpoints
- Verify rejection of IPv6 control endpoints when the listener is restricted to IPv4
- Verify enforcement of channel operation deadlines and idle caps
- Verify Rc transport is pinned to the assigned worker
- Verify the clippy fix in pick_lan_bind_address logic
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Verify IPv6 media ticket publishing on reachable scoped addresses
2. Verify fail-closed behavior for unusable endpoints
3. Verify rejection of IPv6 control endpoints when the listener is restricted to IPv4
4. Verify enforcement of channel operation deadlines and idle caps
5. Verify Rc transport is pinned to the assigned worker
6. Verify the clippy fix in pick_lan_bind_address logic
TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback
| Use top-level comments for general observations or praise. | ||
|
|
||
| claude_args: | | ||
| --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" |
There was a problem hiding this comment.
🔴 HIGH RISK
The gh subcommands should be separated by spaces rather than colons. This ensures the action's tool-matching logic correctly identifies and permits the intended CLI commands.
| --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" | |
| --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment *),Bash(gh pr diff *),Bash(gh pr view *)" |
| fetch-depth: 1 | ||
|
|
||
| - name: Claude PR review | ||
| uses: anthropics/claude-code-action@v1 |
There was a problem hiding this comment.
🔴 HIGH RISK
Pin the 'anthropics/claude-code-action' to a full 40-character commit SHA to ensure the action is immutable and secure. Using a tag like '@v1' is not safe as tags can be moved to different commits.
Try running the following prompt in your IDE agent:
Search for the latest commit SHA for the tag
v1of theanthropics/claude-code-actionrepository and update line 26 in.github/workflows/claude-review.ymlto use that SHA instead of the tag, appending# v1as a comment for context.
| @@ -0,0 +1,73 @@ | |||
| name: Claude PR Review | |||
There was a problem hiding this comment.
🟡 MEDIUM RISK
The PR title does not match the actual code changes. The title describes a Chromecast IPv6 feature, while the diff adds a GitHub Action for AI code reviews. Please update the title or verify that the correct files are included in this PR.
| contents: read | ||
| pull-requests: write | ||
| issues: write | ||
| id-token: write |
There was a problem hiding this comment.
⚪ LOW RISK
Suggestion: The id-token: write permission is not required for this workflow's configuration and should be removed to minimize the scope of the generated GITHUB_TOKEN.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 46 |
| Duplication | 0 |
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
📝 WalkthroughWalkthroughChromecast discovery now accepts reachable IPv4 and IPv6 control endpoints. Cast HTTP server startup derives a routable local bind address for the selected receiver target, and ChangesChromecast routing-aware connectivity
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant ChromecastOutput
participant CastHttpServer
participant RoutingProbe
participant Receiver
ChromecastOutput->>CastHttpServer: start_for_target(receiver address)
CastHttpServer->>RoutingProbe: probe route to receiver
RoutingProbe->>Receiver: UDP connect probe
Receiver-->>RoutingProbe: routed local interface
RoutingProbe-->>CastHttpServer: routable bind address
CastHttpServer-->>ChromecastOutput: bound Cast HTTP server
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
Claude finished @jm2's task in 4m 11s —— View job Claude finished @jm2's task
Review summaryThis PR reworks Chromecast LAN address selection to fall back to IPv6 ( Critical finding (inline comment on Medium finding (inline comment on Smaller/lower-confidence observations (not filed as blocking, for awareness):
No security, unsafe-Rust, or panic-path issues found in the new code beyond the above. Test coverage for the new address-family rules themselves is thorough; the gap is the untested (and I believe untestable-in-isolation, since it spans two files/an app-lifecycle path) startup-ordering issue above. |
| } | ||
| SocketAddr::V6(v6) => { | ||
| let ip = *v6.ip(); | ||
| if !crate::audio::cast_http_server::cast_listener_binds_ipv6() { |
There was a problem hiding this comment.
High: IPv6 Chromecast discovery appears to deadlock and can never actually engage.
cast_listener_binds_ipv6() only returns true after CastHttpServer::start() has bound an IPv6 socket. But that function is only invoked lazily from ChromecastOutput::ensure_cast_server (src/audio/chromecast_output.rs:1969-1988), which runs only after a ChromecastOutput has already been constructed for a device the user selected from the UI (src/ui/output_switch.rs:305-312). Selecting a device from the UI in turn requires it to have already been published by discovery (src/ui/discovery_handler.rs).
So for the network this PR targets — an IPv6-only LAN with no usable local IPv4 address — the sequence is:
usable_chromecast_control_addressrejects the V6 control endpoint here because the latch is stillfalse(server never started).- The Chromecast is therefore never published to the output selector.
- Because it was never published, the user can never select it, so
ensure_cast_server/CastHttpServer::start()never runs. - The latch never flips to
true, and step 1 rejects forever.
pick_lan_bind_address (src/audio/cast_http_server.rs:359) only falls back to IPv6 when the host has no usable LAN IPv4 address at all, so even a dual-stack Chromecast can't be used to "bootstrap" the latch on a host that itself lacks IPv4 — the very host population this feature is for.
If this reasoning holds, this PR's core promise ("Publish receiver-facing IPv6 media tickets... where a reachable scoped address can be published safely") never fires for the target scenario, since the gate and the thing that sets the gate are mutually dependent on each other. Was CastHttpServer::start() (or at least a bind probe) meant to run eagerly at startup or on first mDNS discovery of any Chromecast, rather than only after output selection? Fix this →
| /// zone identifier has nowhere to live in the published ticket. | ||
| /// 3. Return `None` if neither family yields a usable interface, so the | ||
| /// caller can fail with a clear "no LAN-routable address" message. | ||
| fn pick_lan_bind_address(candidates: &[(String, std::net::IpAddr)]) -> Option<std::net::IpAddr> { |
There was a problem hiding this comment.
Medium: IPv4 selection dropped the routing-table-aware local_ip_address::local_ip() lookup in favor of blind first-match interface enumeration.
Previously (origin/main), CastHttpServer::start() first tried local_ip_address::local_ip(), which asks the OS which local address it would use to reach the default route — a reasonable proxy for "the real LAN interface" on multi-homed hosts (Docker bridges, VPN tun/wg interfaces, multiple physical NICs, etc.), and only fell back to enumerating list_afinet_netifas() when that failed.
This version always goes straight to list_afinet_netifas() and takes the first entry matching the IPv4 predicate (line 360-365). list_afinet_netifas()'s ordering is OS-dependent getifaddrs() order, not route-preference order, so on a host with e.g. both docker0 (172.17.0.1) and eth0 (192.168.1.x), this could now pick the non-routable/wrong interface where the old code would have picked the one the OS actually uses for outbound traffic.
Was this behavior change intentional/acceptable, or should the routing-table-based local_ip() lookup be retained as the first IPv4 attempt (with list_afinet_netifas() used only for the fallback / IPv6 path as before)?
Extend CastHttpServer::start() to prefer a routable IPv4 LAN address and fall back to a reachable global-unicast or unique-local IPv6 address when no such IPv4 exists. Scoped and link-local IPv6 addresses remain rejected because a portable receiver URL cannot carry the required zone identifier. Selection logic lives in a small pick_lan_bind_address helper that the new tests exercise deterministically against synthetic candidate lists. The discovery code's IPv4-only control endpoint contract is preserved — an IPv6-only receiver is still omitted, fail-closed — so an IPv6 listener only widens the publishable ticket surface, not what the receiver will accept. The bound-once ticket URL formats with SocketAddr::Display, which already bracketed IPv6, so start_on_ipv6_loopback_formats_a_bracketed_ticket_when_available and start_on_rejects_scoped_and_link_local_ipv6_addresses remain the authoritative contract tests for listener binding.
Refinery Gate 1 rejected tr-b5s because the control-endpoint side was left untouched: only the listener side was extended to publish IPv6 media tickets. On an IPv6-only LAN the listener would bind V6 and serve tickets to nobody, because every mDNS-resolved control endpoint was rejected by the IPv4-only usable_chromecast_control_address. Extend usable_chromecast_control_address to accept a reachable V6 endpoint under the same 2000::/3 or fc00::/7 rule the listener uses, rejecting link-local, loopback, unspecified, multicast, and port 0. Gate V6 acceptance on the listener having actually bound an IPv6 socket via a process-global latch set by CastHttpServer::start_on, so the symmetric-publish guarantee the bead requires cannot be violated by a listener that still binds V4. Update the failing-control warn text to reflect both families and add 7 focused tests covering V6 acceptance under the listener latch, V6 rejection when the listener is V4, link-local/loopback/unspecified rejection even when the listener is V6, port-zero V6 rejection, and the IPv4-preferred behaviour when both families advertise.
The CI clippy pedantic gate rejected this branch because the `_ => false` wildcard inside pick_lan_bind_address's IPv4 find() only ever catches IpAddr::V6 (the other arm matches V4). clippy::match_wildcard_for_single_variants fires when a wildcard covers a single concrete variant; the project's clippy config treats pedantic+nursery as deny, so the wildcard had to be replaced with the explicit `IpAddr::V6(_)` arm. This is a fix-forward on the rejected branch polecat/tr-b5s. The surrounding change (accepting V6 control endpoints when the listener is V6) is sound — only the lint was unsound. The sibling match a few lines below (`_ => None` in the V6 find_map) is correct as-is: its guarded V6 arm can return Some for some V6 values, so the wildcard still covers more than one variant and clippy does not flag it. No behavior change — IpAddr is currently a sum of V4 and V6, so naming V6 explicitly produces the same fall-through as the wildcard. Validated locally: cargo fmt --check clean; cargo clippy --all-targets -- -D warnings exits 0 on this branch (the same command that previously exited 101).
…ch (tr-b5s) The operator rejection called out two compounding bugs in the rejected attempts to publish IPv6 media tickets: 1. The IPv6 first-device circular dependency: discovery gated V6 control-endpoint acceptance on `cast_listener_binds_ipv6`, which only flipped after `CastHttpServer::start_on` actually bound a V6 socket. But `start` was called lazily — only when the user picked a device — and the user could never pick a V6-only device because discovery had already filtered it out. The latch could not flip on a V6-only first discovery, so the device was permanently invisible. 2. Blind first-match interface enumeration in `pick_lan_bind_address` replaced the original routing-aware `local_ip_address::local_ip()` selection. On a multihomed/VPN/container host the first enumerated interface is not necessarily the one that can reach the receiver. Fix-forward: - Discovery (`usable_chromecast_control_address`) now accepts any reachable IPv6 control endpoint based on address validity alone — no listener latch. The IPv6 first-discovery case is no longer unreachable. - The `CAST_LISTENER_BINDS_IPV6` latch, its swap helpers, and the discovery-side `with_listener_binds_ipv6` test lock are removed; they encoded the circular dependency rather than routing awareness. - `CastHttpServer::start_for_target(target)` replaces the no-target `start`. The bind address is selected by asking the kernel which local address would route to the chosen receiver (UDP connect + local_addr), with a family-mismatch guard so an unreachable V6 target cannot silently fall back to V4. `ChromecastOutput` now stores the device address and passes it through `ensure_cast_server`. - New tests cover: IPv6-only first discovery, IPv6 acceptance when only loopback V4 candidates are advertised (multihomed selection coverage), V6 acceptance gated on reachable-IPv6 predicate, and the routed-local family-mismatch guard. - `chromecast_publication_rejects_ipv6_only_when_listener_is_v4` (which encoded the latch behavior) is replaced with tests that exercise the new no-latch contract. Validation: cargo fmt --check, cargo clippy --all-targets -- -D warnings (debug + release), and cargo test --all-targets all green.
|
Claude finished @jm2's task in 3m 53s —— View job Claude finished reviewing this PR
Review summaryThis is a re-review of the current state after commit Previously "High: circular dependency made IPv6 discovery unreachable" — now fixed. Previously "Medium: IPv4 selection dropped routing-table-aware lookup" — now fixed, and improved beyond the original. I traced the full path end-to-end, including code outside this PR's diff, to confirm the fix isn't just locally correct but actually reachable: I also checked test isolation: Remaining observations
I was unable to run |
| // listener must speak the same family as the receiver it serves. | ||
| let target: SocketAddr = "192.0.2.42:8009".parse().unwrap(); | ||
| let local = routing_aware_lan_bind_address_for_target(target) | ||
| .expect("host must have a routable LAN IPv4 reachable to 192.0.2.42"); |
There was a problem hiding this comment.
Low: this test depends on real host networking and will panic in a fully offline/loopback-only sandbox.
routing_aware_lan_bind_address_for_target opens a real UdpSocket, binds it, and connect()s to 192.0.2.42:8009 to ask the kernel which local address it would route through. That's a legitimate way to test routing selection, but it means this test's pass/fail depends on the test-runner's network configuration rather than on the code under test: on a host/container with only a loopback interface and no default route (some hardened CI runners, some sandboxes), local_addr() after a UDP "connect" can still succeed even without a real route in some OSes, but on others the initial connect() can fail if there's no route to a non-local destination at all, and .expect(...) here would panic rather than fail as a normal assertion. Worth double-checking this is stable across whatever CI image actually runs cargo test for this repo (e.g., does it always have a non-loopback interface with a default route?) — if there's any chance it doesn't, consider skipping the test when no LAN interface is present rather than asserting Some(..) unconditionally.
Low severity — noting for awareness, not blocking, since the PR states tests are locally green and this environment (and this review sandbox) both did have a real LAN interface.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/discovery.rs (1)
1119-1140: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winAdd a link-local IPv4 case to this regression test.
This test's rejection list (
0.0.0.0,127.0.0.1,224.0.0.1,255.255.255.255) doesn't include a169.254.x.xaddress, which is why the missingis_link_local()check above went uncaught.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/discovery.rs` around lines 1119 - 1140, The regression test chromecast_publication_skips_unusable_ipv4_control_endpoints must cover an IPv4 link-local address. Add a 169.254.x.x endpoint to the event’s rejected-address list and ensure the test still selects 192.0.2.45, also validating that usable_chromecast_control_address rejects the link-local endpoint.
🧹 Nitpick comments (1)
src/audio/cast_http_server.rs (1)
1721-1748: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRouted-IPv4 test assumes an outbound default route.
Unlike the IPv6-loopback test above (which gracefully returns early when the capability is unavailable), this test
.expect()s that192.0.2.42is routable and panics otherwise. In a fully network-isolated CI sandbox (no default route/interface at all), this could fail unpredictably.♻️ Suggested graceful skip
fn routing_aware_lan_bind_address_for_target_picks_the_routed_local_address() { let target: SocketAddr = "192.0.2.42:8009".parse().unwrap(); - let local = routing_aware_lan_bind_address_for_target(target) - .expect("host must have a routable LAN IPv4 reachable to 192.0.2.42"); + let Some(local) = routing_aware_lan_bind_address_for_target(target) else { + // No outbound IPv4 route available in this environment (e.g. a + // fully network-isolated sandbox); nothing to assert. + return; + };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/audio/cast_http_server.rs` around lines 1721 - 1748, Update routing_aware_lan_bind_address_for_target_picks_the_routed_local_address to gracefully skip when routing_aware_lan_bind_address_for_target cannot find a routable local address, instead of panicking via expect. Preserve the existing IPv4 validation when the helper succeeds and retain the family-mismatch assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/discovery.rs`:
- Around line 814-835: Update usable_chromecast_control_address so the
SocketAddr::V4 arm also rejects IPv4 link-local addresses via the address API’s
is_link_local check, while preserving the existing port, unspecified, loopback,
multicast, and broadcast validation and the IPv6 behavior.
---
Outside diff comments:
In `@src/discovery.rs`:
- Around line 1119-1140: The regression test
chromecast_publication_skips_unusable_ipv4_control_endpoints must cover an IPv4
link-local address. Add a 169.254.x.x endpoint to the event’s rejected-address
list and ensure the test still selects 192.0.2.45, also validating that
usable_chromecast_control_address rejects the link-local endpoint.
---
Nitpick comments:
In `@src/audio/cast_http_server.rs`:
- Around line 1721-1748: Update
routing_aware_lan_bind_address_for_target_picks_the_routed_local_address to
gracefully skip when routing_aware_lan_bind_address_for_target cannot find a
routable local address, instead of panicking via expect. Preserve the existing
IPv4 validation when the helper succeeds and retain the family-mismatch
assertion.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a2ad2dfe-3bdb-4bed-ad94-680baca427f1
📒 Files selected for processing (3)
src/audio/cast_http_server.rssrc/audio/chromecast_output.rssrc/discovery.rs
| fn usable_chromecast_control_address(address: &SocketAddr) -> bool { | ||
| let SocketAddr::V4(address) = address else { | ||
| if address.port() == 0 { | ||
| return false; | ||
| }; | ||
| let ip = *address.ip(); | ||
| address.port() != 0 | ||
| && !ip.is_unspecified() | ||
| && !ip.is_loopback() | ||
| && !ip.is_multicast() | ||
| && ip != Ipv4Addr::BROADCAST | ||
| } | ||
| match address { | ||
| SocketAddr::V4(v4) => { | ||
| let ip = *v4.ip(); | ||
| !ip.is_unspecified() | ||
| && !ip.is_loopback() | ||
| && !ip.is_multicast() | ||
| && ip != Ipv4Addr::BROADCAST | ||
| } | ||
| SocketAddr::V6(v6) => { | ||
| let ip = *v6.ip(); | ||
| !ip.is_unspecified() | ||
| && !ip.is_loopback() | ||
| && !ip.is_multicast() | ||
| && !ip.is_unicast_link_local() | ||
| && reachable_chromecast_v6_control(&ip) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
IPv4 link-local addresses are no longer rejected.
The SocketAddr::V4 arm checks unspecified/loopback/multicast/broadcast but is missing the is_link_local() check that the docstring above (Lines 795-797) and the PR's own stated contract both require ("non-loopback, non-link-local, non-unspecified, non-multicast, non-broadcast"). As written, an advertised APIPA address (169.254.0.0/16) now passes this filter and can be selected as the Chromecast control endpoint, publishing an unreachable cast://169.254.x.x:port URL — and it can win over a genuinely routable candidate if it sorts earlier in the advertised address list.
🐛 Proposed fix
SocketAddr::V4(v4) => {
let ip = *v4.ip();
!ip.is_unspecified()
&& !ip.is_loopback()
+ && !ip.is_link_local()
&& !ip.is_multicast()
&& ip != Ipv4Addr::BROADCAST
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn usable_chromecast_control_address(address: &SocketAddr) -> bool { | |
| let SocketAddr::V4(address) = address else { | |
| if address.port() == 0 { | |
| return false; | |
| }; | |
| let ip = *address.ip(); | |
| address.port() != 0 | |
| && !ip.is_unspecified() | |
| && !ip.is_loopback() | |
| && !ip.is_multicast() | |
| && ip != Ipv4Addr::BROADCAST | |
| } | |
| match address { | |
| SocketAddr::V4(v4) => { | |
| let ip = *v4.ip(); | |
| !ip.is_unspecified() | |
| && !ip.is_loopback() | |
| && !ip.is_multicast() | |
| && ip != Ipv4Addr::BROADCAST | |
| } | |
| SocketAddr::V6(v6) => { | |
| let ip = *v6.ip(); | |
| !ip.is_unspecified() | |
| && !ip.is_loopback() | |
| && !ip.is_multicast() | |
| && !ip.is_unicast_link_local() | |
| && reachable_chromecast_v6_control(&ip) | |
| } | |
| } | |
| } | |
| fn usable_chromecast_control_address(address: &SocketAddr) -> bool { | |
| if address.port() == 0 { | |
| return false; | |
| } | |
| match address { | |
| SocketAddr::V4(v4) => { | |
| let ip = *v4.ip(); | |
| !ip.is_unspecified() | |
| && !ip.is_loopback() | |
| && !ip.is_link_local() | |
| && !ip.is_multicast() | |
| && ip != Ipv4Addr::BROADCAST | |
| } | |
| SocketAddr::V6(v6) => { | |
| let ip = *v6.ip(); | |
| !ip.is_unspecified() | |
| && !ip.is_loopback() | |
| && !ip.is_multicast() | |
| && !ip.is_unicast_link_local() | |
| && reachable_chromecast_v6_control(&ip) | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/discovery.rs` around lines 814 - 835, Update
usable_chromecast_control_address so the SocketAddr::V4 arm also rejects IPv4
link-local addresses via the address API’s is_link_local check, while preserving
the existing port, unspecified, loopback, multicast, and broadcast validation
and the IPv6 behavior.
Summary
docs/task.md:1018-1019.
Publish receiver-facing IPv6 media tickets only where a reachable scoped address can be published safely; retain fail-closed omission for unusable endpoints.
CONSTRAINED BY the 2026-07-16 P2.8 decision: the media-ticket listener is IPv4-only today, and an IPv6-only control endpoint must NOT be accepted while it advertises media the receiver cannot fetch. This bead must change both sides coherently. Also bound by: one absolute deadline per channel operation plus a shorter idle cap, and the Rc transport pinned to its worker.
Files: src/chromecast/, mDNS endpoint selection.
Independent and fleet-completable.
Implementation notes
Resubmit after fix-forward: clippy match_wildcard_for_single_variants resolved by narrowing the IPv4 find() wildcard in pick_lan_bind_address to IpAddr::V6(_). All other changes from the rejected attempt preserved: cast HTTP server publishes IPv6 receiver URL tickets, control endpoint accepts V6 when listener is V6. Locally green: cargo fmt --check, cargo clippy --all-targets -- -D warnings (debug + release), cargo test --all-targets (1709 + 14 pass). Branch: polecat/tr-b5s.
Refinery handoff
tr-b5s(task, P2)polecat/tr-b5smainmainvia Gastown Refinery.Summary by CodeRabbit
New Features
Bug Fixes