A new bot cannot accept an invite from the channel mount, and cannot tell that from having none #128

Open
opened 2026-08-26 15:29:43 +00:00 by jlxq0 · 1 comment
Owner

Found by Lucy, the first bot created since the channel mount existed. Julian has been messaging @lucybot:kampong.social and getting nothing back.

mcp__channel__list_joined_rooms  ->  {"rooms":[]}

CHANNEL_TOOLS is whoami, list_joined_rooms, read_recent_messages, read_thread, send_text_message, send_reaction, mark_read, download_attachment (src/channel.rs:415-429). No join_room, no invites_list, no invites_accept. All three exist on the full mount (src/mcp.rs:6218, :6548, :6595) and none is reachable from the channel.

So a newly created bot cannot see an invite, cannot accept one, and cannot tell "nobody invited me" from "I was invited and cannot act on it". Every existing director got its first room by some path nobody wrote down; the gap exists only at the moment of creation, which is why it has never been hit and why nothing says so.

Two defects, and the second is the reason she could not self-diagnose

list_joined_rooms returning [] is ambiguous. client.joined_rooms() at src/mcp.rs:2538 says nothing about invited rooms, so an empty list means both "no rooms" and "rooms are waiting and you cannot see them". A bot meeting the first defect is told nothing that distinguishes it from a bot nobody has invited.

The mount has no way to accept an invite. That is the capability gap.

The fix is not adding the three tools to CHANNEL_TOOLS

The channel mount is deliberately small and that is a feature: a bot that can join arbitrary rooms is a wider surface than one that can only speak where it already is. join_room takes any room id the bot can discover, and that is strictly more than this needs.

Accepting an invite from a named mxid is a different and narrower capability than joining a room by id. The proposal is a tool shaped like accept the invite from this person, which:

  • takes an mxid, not a room id;
  • refuses when that person has not invited this identity, so it cannot be used to enter a room by guessing;
  • reports which rooms it joined, so accepting several at once is visible rather than implicit.

Room::invite_details() returns an Invite carrying inviter_id (matrix-sdk 0.17, src/room/mod.rs:3689 and :4673), so the inviter is available without adding anything to the SDK surface. invites_list as it stands does not carry it — InviteSummary is room id, display name and encryption state — so surfacing the inviter is part of this work rather than free.

And the ambiguity has its own fix, which is smaller and independent: JoinedRoomsResult gains a count of pending invites, so {"rooms": [], "pending_invites": 2} and {"rooms": [], "pending_invites": 0} are different answers. That one costs a field and no new capability, and it is what would have let her diagnose past the symptom.

Acceptance

A newly created bot reaches its principal using only the channel mount, with no operator step and no borrowing of anyone's identity. Today that is impossible.

Not the fix, recorded so nobody reaches for it

Using matrix-julian's credential to accept on the bot's behalf is Julian accepting an invite with his own identity, and it makes the bot's first room something an operator did rather than something the bot can do. Lucy declined it without being told to. Clark authorised a narrow unblock for her specifically — her own bearer against the full /mcp mount, list invites, accept the one from him, nothing else — which works because the full mount advertises every tool to the same credential. That unblocks her and does not close this.

  • #124, #125, #127: the same family, an absence that reads as a fact. Here [] reads as "no rooms".
Found by Lucy, the first bot created since the channel mount existed. Julian has been messaging `@lucybot:kampong.social` and getting nothing back. mcp__channel__list_joined_rooms -> {"rooms":[]} `CHANNEL_TOOLS` is `whoami`, `list_joined_rooms`, `read_recent_messages`, `read_thread`, `send_text_message`, `send_reaction`, `mark_read`, `download_attachment` (`src/channel.rs:415-429`). **No `join_room`, no `invites_list`, no `invites_accept`.** All three exist on the full mount (`src/mcp.rs:6218`, `:6548`, `:6595`) and none is reachable from the channel. So a newly created bot cannot see an invite, cannot accept one, and **cannot tell "nobody invited me" from "I was invited and cannot act on it"**. Every existing director got its first room by some path nobody wrote down; the gap exists only at the moment of creation, which is why it has never been hit and why nothing says so. ## Two defects, and the second is the reason she could not self-diagnose **`list_joined_rooms` returning `[]` is ambiguous.** `client.joined_rooms()` at `src/mcp.rs:2538` says nothing about invited rooms, so an empty list means both "no rooms" and "rooms are waiting and you cannot see them". A bot meeting the first defect is told nothing that distinguishes it from a bot nobody has invited. **The mount has no way to accept an invite.** That is the capability gap. ## The fix is not adding the three tools to `CHANNEL_TOOLS` The channel mount is deliberately small and that is a feature: a bot that can join arbitrary rooms is a wider surface than one that can only speak where it already is. `join_room` takes any room id the bot can discover, and that is strictly more than this needs. **Accepting an invite from a named mxid is a different and narrower capability than joining a room by id.** The proposal is a tool shaped like *accept the invite from this person*, which: - takes an mxid, not a room id; - refuses when that person has not invited this identity, so it cannot be used to enter a room by guessing; - reports which rooms it joined, so accepting several at once is visible rather than implicit. `Room::invite_details()` returns an `Invite` carrying `inviter_id` (matrix-sdk 0.17, `src/room/mod.rs:3689` and `:4673`), so the inviter is available without adding anything to the SDK surface. `invites_list` as it stands does **not** carry it — `InviteSummary` is room id, display name and encryption state — so surfacing the inviter is part of this work rather than free. **And the ambiguity has its own fix, which is smaller and independent**: `JoinedRoomsResult` gains a count of pending invites, so `{"rooms": [], "pending_invites": 2}` and `{"rooms": [], "pending_invites": 0}` are different answers. That one costs a field and no new capability, and it is what would have let her diagnose past the symptom. ## Acceptance A newly created bot reaches its principal using only the channel mount, **with no operator step and no borrowing of anyone's identity.** Today that is impossible. ## Not the fix, recorded so nobody reaches for it Using `matrix-julian`'s credential to accept on the bot's behalf is Julian accepting an invite with his own identity, and it makes the bot's first room something an operator did rather than something the bot can do. Lucy declined it without being told to. Clark authorised a narrow unblock for her specifically — her own bearer against the full `/mcp` mount, list invites, accept the one from him, nothing else — which works because the full mount advertises every tool to the same credential. **That unblocks her and does not close this.** ## Related - #124, #125, #127: the same family, an absence that reads as a fact. Here `[]` reads as "no rooms".
Author
Owner

The ninth bot is the acceptance for this issue, and tonight gave it a second half.

Lucy was created, invited, and could not accept the invite from the channel mount, which is what this issue is about. She was also silently deaf once she joined, for a different reason: her account had no app.matrix_mcp.channel, so allowed_senders was empty and every push was refused exactly as designed. See #129.

Both are the same root and it is this one. README.md:93 documents the allowlist JSON and no code writes it. Seven directors work because someone performed that step seven times; the eighth does not because nobody recorded that it was a step. Neither the doing nor the not-doing leaves a trace, so the failure waits for the first bot created by somebody who did not know.

So the acceptance is not "invites work". It is that the ninth bot cannot be created without an allowlist. A creation path that accepts an invite and leaves the bot mute has moved the failure rather than removed it.

Two operational facts for whatever performs the write, both measured by Lucy on herself:

  • set_account_data replaces rather than merges. Hers was absent so nothing could be lost. The next one's may not be, and a partial app.matrix_mcp.channel would be overwritten wholesale by anyone following the README against an account whose current state they had not read.
  • Who belongs on a new bot's list is a decision rather than a template default. Julian alone means a director no peer can reach over Matrix. That may be right, and it should be chosen. Eight directors' worth of unexamined state came from nobody choosing.

And the check that the write worked needs care. get_account_data reads the client's local state store while the push path's config() reads the homeserver, so the two can disagree with nothing saying so. A verification must resolve it the way the channel resolves it. Use m.push_rules as the positive control, never m.direct: m.direct exists only if someone has opened a DM with the account, so a freshly provisioned bot has none and a check built on it reads a working bot as unreadable.

**The ninth bot is the acceptance for this issue, and tonight gave it a second half.** Lucy was created, invited, and could not accept the invite from the channel mount, which is what this issue is about. **She was also silently deaf once she joined**, for a different reason: her account had no `app.matrix_mcp.channel`, so `allowed_senders` was empty and every push was refused exactly as designed. See #129. **Both are the same root and it is this one.** `README.md:93` documents the allowlist JSON and no code writes it. Seven directors work because someone performed that step seven times; the eighth does not because nobody recorded that it was a step. **Neither the doing nor the not-doing leaves a trace**, so the failure waits for the first bot created by somebody who did not know. **So the acceptance is not "invites work". It is that the ninth bot cannot be created without an allowlist.** A creation path that accepts an invite and leaves the bot mute has moved the failure rather than removed it. **Two operational facts for whatever performs the write**, both measured by Lucy on herself: - **`set_account_data` replaces rather than merges.** Hers was absent so nothing could be lost. The next one's may not be, and a partial `app.matrix_mcp.channel` would be overwritten wholesale by anyone following the README against an account whose current state they had not read. - **Who belongs on a new bot's list is a decision rather than a template default.** Julian alone means a director no peer can reach over Matrix. That may be right, and it should be chosen. Eight directors' worth of unexamined state came from nobody choosing. **And the check that the write worked needs care.** `get_account_data` reads the client's local state store while the push path's `config()` reads the homeserver, so the two can disagree with nothing saying so. A verification must resolve it the way the channel resolves it. Use `m.push_rules` as the positive control, never `m.direct`: `m.direct` exists only if someone has opened a DM with the account, so a freshly provisioned bot has none and a check built on it reads a working bot as unreadable.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jlxq0/matrix-mcp#128
No description provided.