feat(channel): let the mount open an attachment, not just name it #113
No reviewers
Labels
No labels
blocked
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
waiting-on-julian
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jlxq0/matrix-mcp!113
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "channel-download"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
download_attachmentwas not missing, it was unexposed.CHANNEL_TOOLSlistedseven tools and it was not one of them, so after #107 a channel session is told
attachment="m.image" filename="..."and has no way to open the file. This addsit to the mount and tells the model it can fetch.
Closes #111
The two questions the issue asked
Does it decrypt an E2EE
fileobject? Yes, and not from the doc comment.Media::get_media_contentmatchesMediaSource::Encrypted(file)and runsAttachmentDecryptor::new(cursor, file)before returning(matrix-sdk 0.17
src/media.rs:450-478).TimelineEvent::raw()returnsd.eventforTimelineEventKind::Decrypted, so themsgtypematch seesplaintext; an
UnableToDecryptevent has nomsgtypeand fails with"event content is not a room message" rather than handing back ciphertext.
That is true only while the
e2e-encryptionfeature is on: the decrypt block isinside
#[cfg(feature = "e2e-encryption")], and with the feature off the samecall returns the ciphertext, which the tool base64-encodes and reports as the
file. Nothing errors and a test asserting a non-empty body still passes, so it
is now a Known Pitfall in
AGENTS.md.Does the instructions string need to name the tool? Yes. The schema tells
the model the tool exists; the instructions are where it learns that an
attachment=attribute means there is something to fetch. Without that itreports the filename and stops, which from the far end reads exactly like not
having the tool.
The trap, and the test that can see it
channel_mount_offers_only_the_scoped_toolsasserts advertised is a subset ofCHANNEL_TOOLS. That direction cannot see a name added to the const and neverwired to the mount: the advertised list is simply shorter and every entry in it
still checks out. The symptom is the tool being listed and every call failing.
every_channel_tool_is_actually_advertisedruns the other direction over thereal router. Negative controls, all run:
CHANNEL_TOOLS, wired to nothingdownload_attachmentremoved fromCHANNEL_TOOLSa_session_told_a_file_arrived_can_open_itfailsa_session_told_a_file_arrived_can_open_itfailsa_session_told_a_file_arrived_can_open_itis const-against-const and says so inits own comment: it cannot observe whether the fetch works.
Gates
cargo fmt --all --check,cargo clippy --all-targets --all-features --locked -- -D warningsand
cargo test --all-features --lockedall green on rustc 1.98.0, which iscurrent stable and what the runner installs. 208 tests pass.
Note for anyone reproducing: this tree does not lint below clippy 1.98 for
reasons unrelated to this PR — see #112.
Not done here
The acceptance observation is Julian sending one screenshot into
!nJqaJVNKzmgkUjjSLE:kampong.socialand the session reporting what is visiblyin it. That needs the deployed server, so it happens after
v0.10.3. Neithertest in this PR can stand in for it: a deferred schema can be present while the
fetch fails, and base64 of an encrypted blob is bytes too.
Codex, reviewing the mount change, found the comment I wrote next to CHANNEL_TOOLS was false: it claimed the tool takes a room_id the identity must be joined to. `client.get_room()` reads the SDK's local state store, which holds invited, left, knocked and banned rooms, so `Some` means the identity has heard of the room. `read_thread` and `room_info` both guard on RoomState::Joined for exactly this reason; this one did not, while emitting "not joined to {room_id}" as the error for the case it never checked. Verified rather than taken from the review: matrix-sdk-base 0.17 client.rs:1037 forwards straight to `state_store.room()`, which is state-agnostic. This also makes #111's stated negative control true — an event in a room the identity is not joined to is now refused — where before it held only for a room the store had never seen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0176noPigX2cwByDtRcafzyiCross-engine review (Codex, read-only) on
git diff main...channel-download. Three questions asked, one real finding.Wrong claim in my own comment, now fixed in
4e063de. I wrote next toCHANNEL_TOOLSthat the tool "takes aroom_idthe identity must be joined to". False.client.get_room()forwards tostate_store.room()(matrix-sdk-base 0.17client.rs:1037), which is state-agnostic and returns invited, left, knocked and banned rooms.download_attachmenthad noRoomState::Joinedguard whileread_thread(src/mcp.rs:2589) androom_info(src/mcp.rs:3258) both have one, and it emittednot joined to {room_id}as the error for a case it never checked. Added the guard, matching the sibling idiom. That makes the issue's stated negative control true rather than nearly true.Test wiring: confirmed.
mas_backed_routerbuilds through the productionrouter(...), which nests the channel service at/channel;tool_names_onissues realinitializeandtools/listoverRouter::oneshot.every_channel_tool_is_actually_advertisedcannot pass by reading the const.Instruction-injection surface: none added. The new text is static. Filenames go through
build_params→attr_escape; captions go throughcontent_sandbox::evaluateand land inside<matrix:message trust="external">.Gates re-run after the guard on rustc 1.98.0: fmt, clippy
-D warnings, 208 tests, all green.The joined-room guard is asserted by code inspection and the sibling pattern, not by a test — the three tools that already have it have no test either, and one would need a
Clientwith a populated state store. Worth its own issue if we want it covered.