Skip to content

Propose ADR 011: reveal a user's Matrix id only to users who share a realm with them - #6921

Open
lukemelia wants to merge 9 commits into
mainfrom
adr-011-matrix-ids-private
Open

lukemelia wants to merge 9 commits into
mainfrom
adr-011-matrix-ids-private

Conversation

@lukemelia

Copy link
Copy Markdown
Contributor

Background and Goal

Boxel must not show one user's Matrix id to another. The exception is users who share a realm. This PR proposes ADR 011 to record that rule, so that reviewers and agents can check new designs against it.

#6906 is what prompted it. Its shared room for a world-readable realm's events shows every subscriber to every other, through the room's member list and the joins in its timeline. Synapse has no setting that hides room members from each other.

Where to start

  • §1.1–§1.3: the rule. Users named in a grant on the same realm may see each other's ids. Readers of a world-readable realm see no one, and no one sees them.
  • §1.4: what realm URLs may reveal. A realm's URL may contain its owner's username. Only two kinds of realm list are allowed:
    • the realms a user is named on in a grant;
    • world-readable realms that aren't published realms.
  • §1.5–§1.6: how the rule is kept. It controls who shares a room, and the homeserver blocks the other ways to find a user: the user directory, profile lookups, invites and presence.

Key decisions and non-obvious mechanics

  • Please check the first paragraph of the Rationale. It states the team's reason for the rule in my own words.
  • §1.6 applies to the production homeserver too, whose config lives outside this repository. That config doesn't meet §1.6 yet. CS-14259 tracks it, along with the Remediation entries for the homeserver templates, the invite-check module and the realm server's profile checks.
  • The profile-lookup settings can't ship first. The realm server checks whether an account exists by looking up its profile. Under §1.6 that lookup fails, which would withhold users grants, so the realm server's check has to change first.
  • InviteUserToRoomTool is listed under Remediation. CS-14259 also tracks it.

🤖 Generated with Claude Code

https://claude.ai/code/session_019vCUo5Tv7Mu8Bor9YUFjJm

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019vCUo5Tv7Mu8Bor9YUFjJm
@lukemelia
lukemelia marked this pull request as ready for review October 22, 2026 15:58
@lukemelia
lukemelia requested review from a team, christse, ef4, habdelra and jurgenwerk October 22, 2026 15:58
@habdelra

This comment has been minimized.


When two end users each hold a realm permission grant that names them in the
same realm, Boxel MAY reveal each one's Matrix id to the other. This includes
the realm owner. The `'*'` grant names no one, so it does not count.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

§1.2 excludes only the '*' grant, but ADR 006 §1.1 also defines a users subject (any signed-in user), which names no one either. As written, a design can argue that every signed-in user of a users-readable realm "holds a grant" there, so §1.2 would let all of them see each other, which on one homeserver is effectively everyone. Suggest: "The '*' and users grants name no one, so they do not count."


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Done in bfc2651. §1.2 now reads "The '*' and users grants name no one, so they do not count." The Rationale gives the reason: on one homeserver, users is close to everyone.


### 1.3. Reading a world-readable realm reveals no one

Reading a world-readable realm, or subscribing to its events, MUST NOT reveal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

§1.3 covers only world-readable ('*') realms. A realm readable through a users grant has the same many-unrelated-readers shape, and a shared events room for it would leak readers to each other just as #6906 does. Unless §1.2 explicitly excludes users, nothing here forbids that room. Consider widening §1.3 to "a realm readable through a '*' or users grant".


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Done in bfc2651. §1.3 now defines an open realm as one readable through a '*' or users grant, and applies to reading or subscribing to any open realm. §1.4's list of discoverable realms still covers only world-readable ones. Whether users-readable realms may be listed too is left as a decision for the team.


### 1.6. The homeserver doesn't let users find each other

The homeserver MUST NOT return one end user to another in user directory

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

§1.6 leaves out Synapse's unauthenticated GET /_matrix/client/v3/register/available?username=. Anyone can use it to confirm that a guessed Matrix id exists, which is the oracle the Rationale (L117-119) says the profile settings close. The host depends on it (isUsernameAvailable in packages/host/app/services/matrix-service.ts:2817, called from packages/host/app/components/matrix/register-user.gts:486), so it isn't listed under Remediation, and those files are outside scope. Either add a requirement (rate-limit or gate it) and a Remediation entry, or say why it's accepted.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Addressed in bfc2651 by accepting it explicitly. §1.6 now says the homeserver MAY tell a person registering whether a username is taken, and MUST rate-limit those answers. The Rationale explains why it can't be closed: registration has to refuse a taken username, so hiding it from /register/available (for example with inhibit_user_in_use_error) only moves the answer to the final registration request. At v1.163.0, Synapse already rate-limits /register/available by client address (UsernameAvailabilityRestServlet, sleep_limit=1, reject_limit=1), so no Remediation entry is needed. register-user.gts is now in scope.

of `fetchMatrixProfile`) and
`packages/realm-server/middleware/multi-realm-authorization.ts`
(`hasMatrixProfile`) depend on what §1.6 forbids. They ask the homeserver
for the profile of a user they share no room with, the first without

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"the first without authentication" implies hasMatrixProfile is authenticated, but it isn't. MatrixClient.getProfile (packages/runtime-common/matrix-client.ts:388) calls the same fetchMatrixProfile (L633), which sends no Authorization header. Both lookups therefore fail under require_auth_for_profile_requests. Also, the realm server's user already shares a session DM with every user it authenticates (createDM in handlers/handle-realm-auth.ts:126). So an authenticated lookup from the realm server's client would still pass limit_profile_requests_to_users_who_share_rooms. That makes the premise "a user they share no room with" and the conclusion "they need another way to check an account" look overstated. Authenticating the request may be enough for multi-realm-authorization.ts.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Right on both counts; fixed in bfc2651. The entry now says both paths send an unauthenticated request through fetchMatrixProfile. It also separates the two cases. In multi-realm-authorization.ts the user is the signed-in principal, whose account the realm server already verified when it issued their session, so the lookup may not be needed at all. fetchEffectiveRealmPermissions runs in worker processes (capture, run-command, prerender proxy) that hold no Matrix client, so authenticating it isn't a drop-in change. It needs another way to check an account. A new user may also not have the session DM with the realm server yet when the check runs, so relying on that shared room is fragile.

authentication, to learn whether the account exists. Under §1.6 that lookup
fails, and a failed lookup withholds the user's `users` grant. They need
another way to check an account before the homeserver applies §1.6.
- `packages/matrix/support/synapse/modules/` breaks §1.6. It has no module

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ADR 001 §1.6: "Each entry MUST name a file and the requirement it breaks." This entry names a directory (packages/matrix/support/synapse/modules/). It should name the file that will hold the invite check, or the homeserver.yaml files that must load it (the modules: list).


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Fixed in bfc2651. The entry now names the three homeserver.yaml files, which load no module (modules:) that implements user_may_invite.

- packages/host/app/services/matrix-service.ts
- packages/host/app/services/message-service.ts
- packages/host/app/services/realm-server.ts
- packages/host/app/tools/invite-user-to-room.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scope lists only two host tools. Other host code that creates rooms or invites users isn't covered: packages/host/app/services/ai-assistant-panel-service.ts (invite: [...]), packages/host/app/tools/create-submission-workflow.ts, packages/host/app/tools/bot-requests/** and packages/host/app/components/matrix/register-user.gts. A new invite tool under packages/host/app/tools/ wouldn't surface this ADR through scripts/adr-for.mts. Consider packages/host/app/tools/** plus the other room-creating files.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Done in bfc2651. scope now has packages/host/app/tools/** (covering create-submission-workflow.ts, bot-requests/** and any new tool) plus ai-assistant-panel-service.ts and register-user.gts. Checked with node scripts/adr-for.mts packages/host/app/tools/<new-file>.ts, which lists ADR 011.

`user_may_invite`. It MUST NOT put the inviter's profile into an invite
(`include_profile_data_on_invite: false`).

These rules apply to every homeserver Boxel runs, including the configuration

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't reconcile with ADR 004 (federated realm servers, each with its own homeserver), which shares packages/matrix/support/synapse/** and realm-server/** in scope. Under federation, the homeserver's user_may_invite check (L90-92) has to evaluate §1.2 against grants held on other realm servers, which it can't see. Also, allow_profile_lookup_over_federation: false blocks the cross-server profile lookups that federation needs. And homeservers that Boxel doesn't run aren't bound by §1.6 at all. At least cite ADR 004 and state how §1.5/§1.6 apply to cross-homeserver users, or list that as an open question.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Addressed in bfc2651. The Open questions section is reserved for retroactive ADRs (ADR 001 §1.3), so this went into the Decision instead. A new §1.7 cites ADR 004. A design that puts end users of different homeservers in one room, or lets one invite another, MUST first change this ADR to say how the invite check learns grants held on another realm server. The Rationale notes that a homeserver Boxel doesn't run can't be held to §1.6. It also notes that allow_profile_lookup_over_federation: false doesn't block ADR 004's identity check, which uses /_matrix/federation/v1/openid/userinfo rather than profile lookups.


### 1.2. Users who share a realm may see each other

When two end users each hold a realm permission grant that names them in the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"a realm permission grant that names them" is ambiguous with ADR 006 §1.3: a RealmPolicy grant can also name a user (for example an actor() predicate). It isn't clear whether a user who gets access only through a policy grant "shares a realm" under §1.2. Say explicitly whether policy grants count, since the invite-check module and the Remediation work will need one answer.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Decided in bfc2651: policy grants don't count. §1.2 now says a RealmPolicy grant (ADR 006 §1.3) doesn't count, even one whose predicate matches a single user. Only a permission row whose subject is the user names them. The Rationale gives the reason. The homeserver's invite check can't evaluate a predicate, and a predicate can match as many unrelated users as users does. This also matches #6927, whose room holds only the grantees the permission rows name.

Matrix gives no way to hide a room's members from each other. A member can
list the room's members, and every join is an event in the room's timeline,
which each member receives through sync. Synapse has no setting that changes
this, as of v1.164.0rc1. Blocking the member-list endpoints at a proxy leaves the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cites Synapse v1.164.0rc1, but the repo pins matrixdotorg/synapse:v1.126.0 (packages/matrix/support/synapse/index.ts:514). The per-requester filtering that §1.6 relies on (check_username_for_spam receiving the requester id) and the profile settings should be checked against the version Boxel actually runs. If the pinned version's callback has no requester, the directory can only be turned off, not filtered per §1.2.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] The pin is now v1.163.0 (packages/matrix/support/synapse/index.ts:516), and the Rationale now cites that version (7fbb46b). At that tag, check_username_for_spam receives the searcher's id as requester_id (added in 1.122.0, element-hq/synapse#17916), and UserDirectoryHandler.search_users calls it on every result, so the directory can be filtered by §1.2 instead of only turned off. The Rationale now says so. The four settings §1.6 names (require_auth_for_profile_requests, limit_profile_requests_to_users_who_share_rooms, allow_profile_lookup_over_federation and include_profile_data_on_invite) all exist at 1.163.0, and so does user_may_invite(inviter, invitee, room_id). Since 1.163.0rc1 (element-hq/synapse#20231), Synapse refuses to start with limit_profile_requests_to_users_who_share_rooms on and require_auth_for_profile_requests off. The Remediation entry for the homeserver configs now notes that they must be turned on together.

ylm and others added 2 commits October 25, 2026 23:01
…ediation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TobToyDmxQvQyu8azoFwGP
@lukemelia
lukemelia requested a review from habdelra October 26, 2026 04:17

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants