Chat: a file dropped into an ssh peer's chat goes to that peer (_acpmux/file_put)

Merged
TE
16d ago
+1150-7

Some chats run their agent on an ssh peer: this Mac's acpmux mirrors them, named like huge-blue/<name>. Until now, a file dropped or pasted into such a chat that the composer couldn't attach (a binary, a zip, a large text file) got the note that the chat takes no uploads. The Cloud uploader only serves moved chats. The local uploader would save the file on this Mac, where the agent can't open it. Now the file goes to the machine that runs the chat.

How

  • acpmux: _acpmux/file_put {sessionId, fileName, data} → {path}.
    • The daemon that owns the session writes the file owner-only (folder 0700, file 0600) under <ACPMUX_HOME>/uploads/<session id>/<uuid v7>/<fileName>. Two files with the same name never overwrite each other.
    • The method is session-scoped, so for a peer's chat peer_forward.rs carries it to the peer.
    • The parameter is fileName because the forward drops name, which is a session key.
  • Checks, before anything is decoded or forwarded:
    • Names follow the app's AgentPaneUpload.isFileName rules (filePut.name).
    • Each file is at most 10 MiB (filePut.tooLarge). That is 13.4 MiB of base64, under an older peer's 16 MiB WebSocket frame limit. A bigger frame resets the whole link and every chat on it.
    • An unknown or purged chat is refused.
  • Feature detection: initialize lists filePut, and the peer handshake records whether the peer has it. A peer without it gets -32601 with reason filePut.unsupported, and no bytes are sent.
  • The app adds a third uploader, AgentPeerUploads, tried after Cloud and before local.
    • A tab gets it when its chat is on this Mac's acpmux and AgentLaneStore lists the session with a peer.
    • It calls the method through AgentPaneHostProviding.putFile, which goes over this Mac's acpmux unix socket and requires the filePut feature.
    • An older acpmux (here or on the peer) or a missing host answers unsupported, so the page shows its existing note. Size refusals answer too_large; anything else answers upload_failed.
    • The person-gesture rule is unchanged, and localPaths is false for peer chats, so their Finder drops go as bytes.
  • The page is unchanged: it already sends chat.upload and puts the returned paths in the prompt as resource_links.

Remote access analysis (remote_guard)

  • Allowed from: the unix socket, LocalApp (the app's own connection), and a peer forwarding for either. The app sends it over the unix socket after the person's gesture. A LocalApp page could also send it with no gesture; that is accepted, since the page can already prompt the agent, and each call writes at most 10 MiB owner-only into a fresh folder.
  • Refused: from a Web connection, and from a peer request marked via=web (filePut.remote).
  • What the method can do: it runs no commands and starts no processes. It writes only under acpmux's own uploads/<session>/<random>/. The caller picks neither the folder nor any path; the name is one plain file name (no /, no dot file, no control or bidi characters). The name must be one plain path component (on Windows also no :, so a drive prefix can't leave the folder). The session must be one this daemon owns or forwards.
  • Not a new local effect: the peer daemon already runs the agent with the user's own rights, and writing the person's dropped file next to it gives that agent nothing new.

Not in this PR

  • Cleanup: nothing deletes uploads yet, the same as the app's local Chat Uploads folder. A purge keeps them. Deleting them is a follow-up that should cover both folders.
  • #23047's attachment_put: that PR sends images and PDFs as prompt blocks by reference, which is a separate path from chat.upload. If both land, the two paths have no overlap to reconcile in code.

Tests

  • Rust (cargo test -p acpmux --test file_put). The cases:

    • file modes;
    • a bad name;
    • an oversize file;
    • an unknown chat;
    • a Web connection refused;
    • a purged chat refused;
    • a forward to a peer at the full cap;
    • an older peer refused with zero puts sent.

    Also run: peer_integration, remote_guard, remote_guard_refused_families, peer_origin, local_app_origin, prompt_images. Results at this head are in a comment below.

  • Swift: AgentPaneFilePutTests covers the failure mapping, the size cap before send, and the params. AgentPeerUploadsTests covers the per-file replies, the size refusal, and the unsupported mapping. CI is their first compile.

Observed / Not run

  • Rust: ran on huge-blue (Linux) at f98478b4 and 5afc7d1f; the review fixes at 67b0e2e9 ran on the fleet (comment below).
  • Windows: cargo check --target x86_64-pc-windows-gnu -p acpmux passed at 5afc7d1f (run 39586149383).
  • Swift: not compiled or run outside CI.
  • Not run: a live two-machine run in the app (a drop into a real peer chat).

Changelog

Added: Files dropped into a chat that runs on an ssh peer now reach that machine, so the agent can open them.

No capture: no new UI. The only visible change is two error strings in the composer's existing upload note.

Agent session: cosmic-gray

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Files dropped into a chat whose agent runs on an ssh peer now land on that machine, where the agent can open them. Previously the composer refused them or saved them on this Mac, where the peer's agent can't read them.

The new _acpmux/file_put method saves the bytes owner-only under <ACPMUX_HOME>/uploads/<session>/<random>/<name> on the daemon that owns the session, and AgentPeerUploads (the app's third uploader, tried after Cloud and before local) sends a peer chat's chat.upload files through this Mac's acpmux unix socket. Refusals map to the existing composer notes: an acpmux without the method answers unsupported (chat takes no uploads), oversize is refused before any bytes go, and the page is unchanged.

Checks run before anything is decoded or forwarded: plain file name, 10 MiB cap (an older peer reads 16 MiB frames), existing session, peer advertising the filePut feature. The method is allowed only from the unix socket, LocalApp, and peers forwarding for either; Web is refused. Uploads are kept on purge, like the app's local Chat Uploads; cleanup is a follow-up.