Skip to content

Deliver realm grants to running sessions; boot permissioned accounts with a complete workspace list - #6088

Open
FadhlanR wants to merge 10 commits into
mainfrom
realm-creation-host-vs-endpoint
Open

Deliver realm grants to running sessions; boot permissioned accounts with a complete workspace list#6088
FadhlanR wants to merge 10 commits into
mainfrom
realm-creation-host-vs-endpoint

Conversation

@FadhlanR

Copy link
Copy Markdown
Contributor

Problem

The host builds a user's workspace list once at boot from Matrix account data (app.boxel.realm-servers_realm-auth, or legacy app.boxel.realms). Realm permissions live in a separate store (realm_user_permissions), which the running client only reads through that boot-time assembly. Two consequences:

  1. A grant never reaches a signed-in session. Granting a user read+write updates realm_user_permissions but pushes no live signal, so the realm doesn't appear until the user reloads (opening the realm URL directly works, proving the grant is live).
  2. An account with no account-data key boots empty. A user provisioned outside the host sign-up flow has neither account-data key. Boot assembles an empty list regardless of the permissions it holds, and has no personal workspace.

Changes

Boot assembles from permissions (host — matrix-service.ts)

  • When an account has no realm list at all, seed app.boxel.realm-servers with the host's own realm server on first login and persist it, so boot runs the permissions-driven assembly (_realm-auth) instead of falling through to an empty list. Scoped to accounts with no legacy realm list, so existing accounts keep their current assembly + lazy-migration path.
  • In the event-driven refresh, fall back to the own realm server when no trusted-servers entry is persisted, so a live grant still re-assembles.

Live grant signal (realm-server / runtime-common)

  • patchRealmPermissions now notifies each affected user via the existing realms-list-updated session-room event; the host already re-runs _realm-auth on it. Nothing else told a grantee — the realm_index_updated broadcast is server-to-server only.
  • The grafana upsert handler emits the same event, covering the case where its account-data append is a no-op (server already present) and produces no Matrix event on its own.
  • Both emits are best-effort and never roll back a committed grant; delivery is a no-op when the user has no session room (their next login assembles correctly).

Personal workspace on login (host)

  • Provision a personal workspace lazily on login when the user has none, non-blocking. _create-realm emits realms-list-updated to the user, so it surfaces live. Idempotent against a concurrent tab / prior login.

Verification

  • ember-tsc --noEmit clean for all three changed files (remaining errors in a fresh checkout are the known @cardstack/boxel-icons / boxel-ui baseline, none in the edited files).
  • eslint clean (pre-commit hook passed on all three files).

Testing status

Draft — automated tests are the next step. Planned coverage: boot with permissions but no account-data key shows the realm; a live realms-list-updated event adds a newly-granted realm without reload; legacy-list boot yields an equal-or-pruned set; grant write paths emit the event (and no-op cleanly without a session room); personal realm created only when absent.

🤖 Generated with Claude Code

…complete

The workspace list is assembled once at boot from Matrix account data, so a
permission grant never reaches a signed-in session (reload required) and an
account with no account-data key boots to an empty chooser despite holding
permissions.

- Seed app.boxel.realm-servers with the host's own realm server on first login
  when an account has no realm list, so boot assembles from realm permissions
  via _realm-auth instead of an empty list. Fall back to the own realm server
  in the event-driven refresh when no trusted-servers entry is persisted.
- Emit realms-list-updated to each affected user from the permission write
  paths (patchRealmPermissions and the grafana upsert handler) so a running
  session re-derives its list live.
- Provision a personal workspace lazily on login when the user has none.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files      1 suites   2h 30m 49s ⏱️
4 747 tests 4 733 ✅ 14 💤 0 ❌
4 762 runs  4 748 ✅ 14 💤 0 ❌

Results for commit 2a4e8fb.

Realm Server Test Results

    1 files  ±0    212 suites  ±0   1h 20m 45s ⏱️ + 8m 21s
2 790 tests +1  2 790 ✅ +1  0 💤 ±0  0 ❌ ±0 
2 829 runs  +1  2 829 ✅ +1  0 💤 ±0  0 ❌ ±0 

Results for commit 2a4e8fb. ± Comparison against earlier commit 86651fd.

FadhlanR and others added 3 commits September 11, 2026 13:29
- boot-assembly: a keyless account (neither account-data key) with permissions
  assembles its realm from _realm-auth and persists the own realm server into
  app.boxel.realm-servers; a legacy-list account is left on its existing path.
- personal-realm: login provisions a personal workspace when the user has none
  and does not when one already exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Grants a user with an established session room via PATCH /_permissions and
asserts a realms-list-updated event lands in that room; also asserts the grant
succeeds when the grantee has no session room (best-effort push is a no-op).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Provisioning on every boot fired createRealm across the whole test suite —
colliding with the workspace-chooser tests that stub createRealm and polluting
unrelated acceptance tests with "Failed to fetch" console errors, and it would
create stray realms in the matrix e2e app. Needs a narrower trigger and a
harness create-realm mock; deferring to a follow-up. The grant-reaches-session
and boot-from-permissions fixes stand on their own.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FadhlanR
FadhlanR force-pushed the realm-creation-host-vs-endpoint branch from 28d649c to 7c96e5c Compare September 11, 2026 07:51
FadhlanR and others added 6 commits September 11, 2026 15:05
…rkspace

Batch-created accounts (registered straight on Synapse) never go through the
host sign-up flow that creates a personal workspace, so they boot without one.
Add POST /_grafana-ensure-personal-realm?user=<mxid> (grafana-secret auth) that
creates the user's personal realm if absent and pushes realms-list-updated so a
running session shows it without a reload. Idempotent: createRealm rejects a
pre-existing realm, which the endpoint reports as alreadyExisted.

A host boot-time trigger was rejected: after the boot seed a granted Synapse
account and an ordinary test account are indistinguishable at boot (both
keyless, both assemble realms via _realm-auth, both lack a personal realm), so
any boot trigger fires across the whole test suite. The ops-run endpoint is
test-isolated and matches how these accounts are provisioned.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The boot seed flipped every keyless account onto the authoritative
trusted-servers path, which disables the app.boxel.realms AccountData listener
— so an account could no longer add realms (including foreign realm URLs that
_realm-auth cannot serve) via account data. Only commit to the trusted path
and persist the seed once _realm-auth confirms the account holds permissioned
realms; otherwise revert to the legacy assembly so account-data updates keep
driving the list. Fixes the realm-urls matrix e2e regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the ops-run grafana endpoint with automatic host-side provisioning: on
login, when the user has no personal workspace, create one. This is what an
account provisioned outside host sign-up (e.g. registered straight on Synapse)
was missing, and unlike the endpoint it needs no ops action.

The trigger is gated on ENV.hostedEnvironment being staging/production. That
value is the realm-server's serve-time environment and is `local` in both the
QUnit suite and the matrix e2e host, so auto-provisioning never fires in tests
(which is what made a plain boot trigger pollute the suite before). The
provisioning logic is exposed as a public method so a test drives it directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose the auto-provision trigger as `autoProvisionPersonalRealm` (a settable
field) instead of an environment-only getter. Its default still comes from
ENV.hostedEnvironment — the one in-app signal that is `local` in both the QUnit
suite and the matrix e2e host and `staging`/`production` only in a real
deployment — but a test can now flip it on and exercise the real
start()->provision path. Provisioning runs through a dropTask so settled()
waits for it in tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The boot seed for a keyless account decided whether to keep the authoritative
trusted path by checking whether `_realm-auth` returned any realms. But
`_realm-auth` returns the public base/catalog realms for every authenticated
user, so the result was never empty — even for an account with no workspace of
its own. That left every keyless account flipped to
`trustedRealmServersAuthoritative`, which makes the `app.boxel.realms`
AccountData listener ignore updates, so a realm added to that key (e.g. a
foreign realm URL) never appeared without a reload. (Caught by
matrix/tests/realm-urls.spec.ts.)

Base the decision on user-visible realms instead: after the list is assembled,
the public realms have deduped into their base/catalog entries, so
`userRealmIdentifiers` is empty exactly when the account has no workspace of its
own. Revert to the legacy path (and don't persist the seed) in that case; keep
and persist the seed only when a real user realm is present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-ups on the live-grant / boot-assembly work:

- Consolidate session-room event delivery into a single runtime-common
  `createSendEvent` helper and give it stale-room self-heal: on a Matrix
  "not in room" rejection it clears the stored session_room_id so the
  user's next auth re-mints a room the current account is in. The realm
  grant notification and the grafana upsert handler now both route through
  it (deleting a fourth hand-rolled copy), and node-realm's broadcast reuses
  the moved error helpers. Covered by a new self-heal unit test.

- Scope personal-realm auto-provisioning to the user's OWN workspace URL
  (derived the same way the server does) instead of matching any
  `/personal/` realm, so a foreign personal workspace shared with the user
  no longer suppresses provisioning of their own. New integration test.

- Auto-provision on any real deployment (hostedEnvironment !== 'local'),
  not just staging/production, so self-hosted deployments get it too.

- Comment accuracy: the permissions-test grantee choice now documents why
  it faithfully models the production single-account topology; the boot
  auto-provision comment credits the account-data channel that actually
  surfaces the realm for a legacy-path account; the already-exists branch
  is documented as a race backstop behind the own-URL check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FadhlanR
FadhlanR marked this pull request as ready for review September 11, 2026 22:56
@FadhlanR
FadhlanR requested a review from a team September 11, 2026 22:56
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-11T23:00:15.735698Z 2a4e8fb Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a4e8fbd85

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// does on staging, production, and any self-hosted deployment. A settable
// field (not a getter) so a test can flip it on and exercise the real
// start()->provision path.
autoProvisionPersonalRealm = ENV.hostedEnvironment !== 'local';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enable provisioning for self-hosted local deployments

When a self-hosted realm server does not set REALM_SENTRY_ENVIRONMENT, serve-index.ts retains the build default hostedEnvironment: 'local', so this condition disables provisioning there as well as in tests. Consequently, directly provisioned Matrix accounts in ordinary local/self-hosted installations still boot without the personal workspace this change is intended to create. Use an explicit test-mode signal or feature flag rather than treating every local deployment as a test.

Useful? React with 👍 / 👎.

Comment on lines +970 to +972
private ensurePersonalRealmTask = dropTask(async () => {
await this.ensurePersonalRealmForUserIfMissing();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Cancel provisioning when the session ends

If the user logs out or switches accounts while the profile lookup or realm creation is pending, this service-level task survives logout()/resetState(). It can then resume against the replacement Matrix client: the initial username check came from account A, while createPersonalRealmForUser() authenticates and appends account data using account B, potentially creating B's workspace with A's display name or mutating B's realm list. Cancel the task at the session boundary or verify that the initiating user is still current before creating and appending.

Useful? React with 👍 / 👎.

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.

1 participant