acpmux: a peer's media handles are each client's own, and close on the peer when the client goes
Leo-ask: acpmux-media-safety
Leo, 2026-10-21: "we also want acpmux to be able to have images and videos (i think we already had it but... it just wasn't rendering for images. not sure if videos were safe) we want to make it prompt injection safe as well since images to remote is maybe dangerous to remote... we need to look into how t3 code does stuff"
Adopted by p0-machines from a lane that died on cmux16. Rebased onto current feat-cmux-next and cut down to what base still leaves open.
Original intent
From the original body: "It serves #24372's reader as two hub methods, so the app's pane can read a chat's images and videos on the machine that runs the chat", with "A handle works only for the chat it was opened for, on the connection that opened it", and a peer's chat "forwarded to the peer that owns it".
What base already covers
#24372 (842a4f78042) and #24619 (5ff4b0cc043) landed the reader and both routes: _acpmux/media_open / _acpmux/media_range (rpc.rs, requests.rs dispatch, initialize lists them and mediaRead), the per-connection handle table (hub/media_handles.rs), the Web refusal (server/remote_guard.rs, mediaRead.remote), the read-only device refusal, and the refusal of a peer that doesn't list mediaRead (peer.rs supports_media_read, server/peer_forward.rs). The protocol registry, the generated methods.ts and the README already have both methods.
What is still open on base
A peer's chat is forwarded straight through. Every client of this daemon reaches the peer over one link connection, so on the peer they all share the link's handle table:
- one client can range through another client's handle (the open's
handleworks for any client of the forwarding daemon); - one client holding
MAX_HANDLES(8) leaves no room for any other client of that daemon; - a client that goes away leaves its handles holding the link's places until the peer's 2-minute idle sweep.
Fix
server/media_forward.rs(new): each client connection keeps its own table of peers' handles. An open answers a fresh handle of this connection that stands for the peer's; a client holds at mostMAX_HANDLES, counting opens still waiting on the peer; an entry goes with its final range or a refused one; when the connection ends every handle it still holds is closed on the peer (a length-0 range, which the peer refuses and so closes).server/peer_forward.rs: a peer chat's media open or range goes through it, after the existingmediaReadcheck.server/mod.rs:Conn.remote_media, closed at the connection's end. A peer link's own table (ConnAuth::PeerToken) is capped by the daemon'sMAX_OPENonly, since the forwarding daemon caps each of its clients.- A refused range closes the handle on the peer (an awaited length-0 range) before its place here is given back: the refusal may come from the peer's guard before its reader saw the range. An idle entry dropped here is closed on the peer too.
- When the link to the peer ends, the peer closes every handle it held for that link.
Peercounts a link epoch (bumped when a connection ends, and at stop), so an entry opened on a link that is gone holds no place here and serves no range. An open whose link changed while it was out is refused and released. hub/media_handles.rs: a per-table cap,forget_orphan, and a gone chat's handles count toward no cap (the insert and the sweeper drop them).hub/media_read.rs: a range for a chat that no longer resolves closes the handle it names only when that handle's own chat no longer exists, and a purged chat closes only its own handle, so naming another chat's ID never closes a live chat's handle.
Size: 798 changed lines, about half of them tests. To stay under ~800, four tests of what base already does were left out (the initialize listing, the read-only device refusal, and a handle's idle sweep and session binding).
Before / after (fleet, cmux-ci test -- cargo test -p acpmux, Linux)
- Before, at bd9de87b69e (tests only, on current base; run https://github.com/manaflow-ai/cmux/actions/runs/40244215707):
media_of_a_peers_chat_is_read_on_the_peerfails atmedia_read.rs:146. A second client'smedia_rangewith the first client's handle is not refused and returns the peer's bytes ({"data":"AAAAGGZ0eXBpcw==","offset":0,"peer":"b","size":1000}). - Before, at 0a4a53c1145 (the round-1 fix plus the round-2 test, without the round-2 fix; run https://github.com/manaflow-ai/cmux/actions/runs/40245111129):
media_a_range_the_peer_refuses_early_still_closes_its_handle_therefails in round 256. Ranges the peer's guard refuses each leave a handle on the peer, until it answersmedia.handles. - After, at 0a0a0506a0c (run https://github.com/manaflow-ai/cmux/actions/runs/40246435738):
cargo fmt -p acpmux --check,cargo clippy -p acpmux --all-targets -- -D warningsandcargo test -p acpmux mediapass. All 5 intests/media_read.rspass, and so do 21 acpmux unit tests matchingmedia, includingserver::media_forward::tests::a_handle_of_an_ended_peer_link_holds_no_place. That unit test uses the new link epoch, so it has no run at an earlier head. - Review: Codex (gpt-6-astra, xhigh). Round 1 at effbe8b602b found 2 should-fix and 1 nit; round 2 at fe30ed23359 found the ended-link should-fix. All are fixed above.
Not run
The app's pane does not call these routes for a peer's chat yet, so there's no app-level behavior to show.
Changelog
none
Proof
No capture: daemon-only change with no UI.
š¤ Generated with Claude Code
Agent session: Air-Blue